what-core 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/a11y.js +22 -7
- package/dist/animation.js +20 -3
- package/dist/components.js +61 -23
- package/dist/data.js +272 -68
- package/dist/dom.js +325 -89
- package/dist/form.js +112 -44
- package/dist/helpers.js +73 -10
- package/dist/hooks.js +75 -22
- package/dist/index.js +6 -2
- package/dist/reactive.js +202 -28
- package/dist/render.js +716 -0
- package/dist/scheduler.js +10 -5
- package/dist/store.js +19 -8
- package/package.json +4 -1
- package/src/a11y.js +22 -7
- package/src/animation.js +20 -3
- package/src/components.js +61 -23
- package/src/data.js +272 -68
- package/src/dom.js +325 -89
- package/src/form.js +112 -44
- package/src/helpers.js +73 -10
- package/src/hooks.js +75 -22
- package/src/index.js +6 -2
- package/src/reactive.js +202 -28
- package/src/render.js +716 -0
- package/src/scheduler.js +10 -5
- package/src/store.js +19 -8
package/src/reactive.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// What Framework - Reactive Primitives
|
|
2
2
|
// Signals + Effects: fine-grained reactivity without virtual DOM overhead
|
|
3
3
|
|
|
4
|
+
// Dev-mode flag — build tools can dead-code-eliminate when false
|
|
5
|
+
export const __DEV__ = typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production' || true;
|
|
6
|
+
|
|
4
7
|
let currentEffect = null;
|
|
8
|
+
let currentRoot = null;
|
|
5
9
|
let batchDepth = 0;
|
|
6
|
-
let pendingEffects =
|
|
10
|
+
let pendingEffects = [];
|
|
7
11
|
|
|
8
12
|
// --- Signal ---
|
|
9
13
|
// A reactive value. Reading inside an effect auto-tracks the dependency.
|
|
@@ -16,7 +20,7 @@ export function signal(initial) {
|
|
|
16
20
|
function read() {
|
|
17
21
|
if (currentEffect) {
|
|
18
22
|
subs.add(currentEffect);
|
|
19
|
-
currentEffect.deps.
|
|
23
|
+
currentEffect.deps.push(subs); // Track reverse dep for cleanup
|
|
20
24
|
}
|
|
21
25
|
return value;
|
|
22
26
|
}
|
|
@@ -48,19 +52,24 @@ export function computed(fn) {
|
|
|
48
52
|
const inner = _createEffect(() => {
|
|
49
53
|
value = fn();
|
|
50
54
|
dirty = false;
|
|
51
|
-
|
|
52
|
-
}, { lazy: true });
|
|
55
|
+
}, true);
|
|
53
56
|
|
|
54
57
|
function read() {
|
|
55
58
|
if (currentEffect) {
|
|
56
59
|
subs.add(currentEffect);
|
|
57
|
-
currentEffect.deps.
|
|
60
|
+
currentEffect.deps.push(subs);
|
|
58
61
|
}
|
|
59
62
|
if (dirty) _runEffect(inner);
|
|
60
63
|
return value;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
|
|
66
|
+
// When a dependency changes, mark dirty AND propagate to our subscribers.
|
|
67
|
+
// This is how effects that read this computed know to re-run:
|
|
68
|
+
// signal changes → computed._onNotify → computed's subs get notified.
|
|
69
|
+
inner._onNotify = () => {
|
|
70
|
+
dirty = true;
|
|
71
|
+
notify(subs);
|
|
72
|
+
};
|
|
64
73
|
|
|
65
74
|
read._signal = true;
|
|
66
75
|
read.peek = () => {
|
|
@@ -75,10 +84,25 @@ export function computed(fn) {
|
|
|
75
84
|
// Runs a function, auto-tracking signal reads. Re-runs when deps change.
|
|
76
85
|
// Returns a dispose function.
|
|
77
86
|
|
|
78
|
-
export function effect(fn) {
|
|
87
|
+
export function effect(fn, opts) {
|
|
79
88
|
const e = _createEffect(fn);
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
// First run: skip cleanup (deps is empty), just run and track
|
|
90
|
+
const prev = currentEffect;
|
|
91
|
+
currentEffect = e;
|
|
92
|
+
try {
|
|
93
|
+
const result = e.fn();
|
|
94
|
+
if (typeof result === 'function') e._cleanup = result;
|
|
95
|
+
} finally {
|
|
96
|
+
currentEffect = prev;
|
|
97
|
+
}
|
|
98
|
+
// Mark as stable after first run — subsequent re-runs skip cleanup/re-subscribe
|
|
99
|
+
if (opts?.stable) e._stable = true;
|
|
100
|
+
const dispose = () => _disposeEffect(e);
|
|
101
|
+
// Register with current root for automatic cleanup
|
|
102
|
+
if (currentRoot) {
|
|
103
|
+
currentRoot.disposals.push(dispose);
|
|
104
|
+
}
|
|
105
|
+
return dispose;
|
|
82
106
|
}
|
|
83
107
|
|
|
84
108
|
// --- Batch ---
|
|
@@ -96,23 +120,57 @@ export function batch(fn) {
|
|
|
96
120
|
|
|
97
121
|
// --- Internals ---
|
|
98
122
|
|
|
99
|
-
function _createEffect(fn,
|
|
123
|
+
function _createEffect(fn, lazy) {
|
|
100
124
|
return {
|
|
101
125
|
fn,
|
|
102
|
-
deps:
|
|
103
|
-
lazy:
|
|
126
|
+
deps: [], // array of subscriber sets (cheaper than Set for typical 1-3 deps)
|
|
127
|
+
lazy: lazy || false,
|
|
104
128
|
_onNotify: null,
|
|
105
129
|
disposed: false,
|
|
130
|
+
_pending: false,
|
|
131
|
+
_stable: false, // stable effects skip cleanup/re-subscribe on re-run
|
|
106
132
|
};
|
|
107
133
|
}
|
|
108
134
|
|
|
109
135
|
function _runEffect(e) {
|
|
110
136
|
if (e.disposed) return;
|
|
137
|
+
|
|
138
|
+
// Stable effect fast path: deps don't change, skip cleanup/re-subscribe.
|
|
139
|
+
// Effect stays subscribed to its signals from the first run.
|
|
140
|
+
if (e._stable) {
|
|
141
|
+
if (e._cleanup) {
|
|
142
|
+
try { e._cleanup(); } catch (err) {
|
|
143
|
+
if (__DEV__) console.warn('[what] Error in effect cleanup:', err);
|
|
144
|
+
}
|
|
145
|
+
e._cleanup = null;
|
|
146
|
+
}
|
|
147
|
+
const prev = currentEffect;
|
|
148
|
+
currentEffect = null; // Don't re-track deps (already subscribed)
|
|
149
|
+
try {
|
|
150
|
+
const result = e.fn();
|
|
151
|
+
if (typeof result === 'function') e._cleanup = result;
|
|
152
|
+
} finally {
|
|
153
|
+
currentEffect = prev;
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
111
158
|
cleanup(e);
|
|
159
|
+
// Run effect cleanup from previous run
|
|
160
|
+
if (e._cleanup) {
|
|
161
|
+
try { e._cleanup(); } catch (err) {
|
|
162
|
+
if (__DEV__) console.warn('[what] Error in effect cleanup:', err);
|
|
163
|
+
}
|
|
164
|
+
e._cleanup = null;
|
|
165
|
+
}
|
|
112
166
|
const prev = currentEffect;
|
|
113
167
|
currentEffect = e;
|
|
114
168
|
try {
|
|
115
|
-
e.fn();
|
|
169
|
+
const result = e.fn();
|
|
170
|
+
// Capture cleanup function if returned
|
|
171
|
+
if (typeof result === 'function') {
|
|
172
|
+
e._cleanup = result;
|
|
173
|
+
}
|
|
116
174
|
} finally {
|
|
117
175
|
currentEffect = prev;
|
|
118
176
|
}
|
|
@@ -121,37 +179,133 @@ function _runEffect(e) {
|
|
|
121
179
|
function _disposeEffect(e) {
|
|
122
180
|
e.disposed = true;
|
|
123
181
|
cleanup(e);
|
|
182
|
+
// Run cleanup on dispose
|
|
183
|
+
if (e._cleanup) {
|
|
184
|
+
try { e._cleanup(); } catch (err) {
|
|
185
|
+
if (__DEV__) console.warn('[what] Error in effect cleanup on dispose:', err);
|
|
186
|
+
}
|
|
187
|
+
e._cleanup = null;
|
|
188
|
+
}
|
|
124
189
|
}
|
|
125
190
|
|
|
126
191
|
function cleanup(e) {
|
|
127
|
-
|
|
128
|
-
|
|
192
|
+
const deps = e.deps;
|
|
193
|
+
for (let i = 0; i < deps.length; i++) deps[i].delete(e);
|
|
194
|
+
deps.length = 0;
|
|
129
195
|
}
|
|
130
196
|
|
|
131
197
|
function notify(subs) {
|
|
132
|
-
|
|
133
|
-
const snapshot = [...subs];
|
|
134
|
-
for (const e of snapshot) {
|
|
198
|
+
for (const e of subs) {
|
|
135
199
|
if (e.disposed) continue;
|
|
136
200
|
if (e._onNotify) {
|
|
137
201
|
e._onNotify();
|
|
138
|
-
|
|
139
|
-
|
|
202
|
+
} else if (batchDepth === 0 && e._stable) {
|
|
203
|
+
// Inline execution for stable effects: skip queue + flush + _runEffect overhead.
|
|
204
|
+
// Safe because stable effects have fixed deps (no re-subscribe needed).
|
|
205
|
+
const prev = currentEffect;
|
|
206
|
+
currentEffect = null;
|
|
207
|
+
try {
|
|
208
|
+
const result = e.fn();
|
|
209
|
+
if (typeof result === 'function') {
|
|
210
|
+
if (e._cleanup) try { e._cleanup(); } catch (err) {}
|
|
211
|
+
e._cleanup = result;
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (__DEV__) console.warn('[what] Error in stable effect:', err);
|
|
215
|
+
} finally {
|
|
216
|
+
currentEffect = prev;
|
|
217
|
+
}
|
|
218
|
+
} else if (!e._pending) {
|
|
219
|
+
e._pending = true;
|
|
220
|
+
pendingEffects.push(e);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (batchDepth === 0 && pendingEffects.length > 0) scheduleMicrotask();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let microtaskScheduled = false;
|
|
227
|
+
function scheduleMicrotask() {
|
|
228
|
+
if (!microtaskScheduled) {
|
|
229
|
+
microtaskScheduled = true;
|
|
230
|
+
queueMicrotask(() => {
|
|
231
|
+
microtaskScheduled = false;
|
|
232
|
+
flush();
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function flush() {
|
|
238
|
+
let iterations = 0;
|
|
239
|
+
while (pendingEffects.length > 0 && iterations < 100) {
|
|
240
|
+
const batch = pendingEffects;
|
|
241
|
+
pendingEffects = [];
|
|
242
|
+
for (let i = 0; i < batch.length; i++) {
|
|
243
|
+
const e = batch[i];
|
|
244
|
+
e._pending = false;
|
|
245
|
+
if (!e.disposed && !e._onNotify) _runEffect(e);
|
|
140
246
|
}
|
|
141
|
-
|
|
142
|
-
|
|
247
|
+
iterations++;
|
|
248
|
+
}
|
|
249
|
+
if (iterations >= 100) {
|
|
250
|
+
if (__DEV__) {
|
|
251
|
+
const remaining = pendingEffects.slice(0, 3);
|
|
252
|
+
const effectNames = remaining.map(e => e.fn?.name || e.fn?.toString().slice(0, 60) || '(anonymous)');
|
|
253
|
+
console.warn(
|
|
254
|
+
`[what] Possible infinite effect loop detected (100 iterations). ` +
|
|
255
|
+
`Likely cause: an effect writes to a signal it also reads, creating a cycle. ` +
|
|
256
|
+
`Use untrack() to read signals without subscribing. ` +
|
|
257
|
+
`Looping effects: ${effectNames.join(', ')}`
|
|
258
|
+
);
|
|
143
259
|
} else {
|
|
144
|
-
|
|
260
|
+
console.warn('[what] Possible infinite effect loop detected');
|
|
145
261
|
}
|
|
262
|
+
for (let i = 0; i < pendingEffects.length; i++) pendingEffects[i]._pending = false;
|
|
263
|
+
pendingEffects.length = 0;
|
|
146
264
|
}
|
|
147
265
|
}
|
|
148
266
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
267
|
+
// --- Memo ---
|
|
268
|
+
// Eager computed that only propagates when the value actually changes.
|
|
269
|
+
// Reads deps eagerly (unlike lazy computed), but skips notifying subscribers
|
|
270
|
+
// when the recomputed value is the same. Critical for patterns like:
|
|
271
|
+
// memo(() => selected() === item().id) — 1000 memos, only 2 change
|
|
272
|
+
export function memo(fn) {
|
|
273
|
+
let value;
|
|
274
|
+
const subs = new Set();
|
|
275
|
+
|
|
276
|
+
const e = _createEffect(() => {
|
|
277
|
+
const next = fn();
|
|
278
|
+
if (!Object.is(value, next)) {
|
|
279
|
+
value = next;
|
|
280
|
+
notify(subs);
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
_runEffect(e);
|
|
285
|
+
|
|
286
|
+
// Register with current root
|
|
287
|
+
if (currentRoot) {
|
|
288
|
+
currentRoot.disposals.push(() => _disposeEffect(e));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function read() {
|
|
292
|
+
if (currentEffect) {
|
|
293
|
+
subs.add(currentEffect);
|
|
294
|
+
currentEffect.deps.push(subs);
|
|
295
|
+
}
|
|
296
|
+
return value;
|
|
154
297
|
}
|
|
298
|
+
|
|
299
|
+
read._signal = true;
|
|
300
|
+
read.peek = () => value;
|
|
301
|
+
return read;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// --- flushSync ---
|
|
305
|
+
// Force all pending effects to run synchronously. Use sparingly.
|
|
306
|
+
export function flushSync() {
|
|
307
|
+
microtaskScheduled = false;
|
|
308
|
+
flush();
|
|
155
309
|
}
|
|
156
310
|
|
|
157
311
|
// --- Untrack ---
|
|
@@ -165,3 +319,23 @@ export function untrack(fn) {
|
|
|
165
319
|
currentEffect = prev;
|
|
166
320
|
}
|
|
167
321
|
}
|
|
322
|
+
|
|
323
|
+
// --- createRoot ---
|
|
324
|
+
// Isolated reactive scope. All effects created inside are tracked and disposed together.
|
|
325
|
+
// Essential for per-item cleanup in reactive lists.
|
|
326
|
+
export function createRoot(fn) {
|
|
327
|
+
const prevRoot = currentRoot;
|
|
328
|
+
const root = { disposals: [], owner: currentRoot };
|
|
329
|
+
currentRoot = root;
|
|
330
|
+
try {
|
|
331
|
+
const dispose = () => {
|
|
332
|
+
for (let i = root.disposals.length - 1; i >= 0; i--) {
|
|
333
|
+
root.disposals[i]();
|
|
334
|
+
}
|
|
335
|
+
root.disposals.length = 0;
|
|
336
|
+
};
|
|
337
|
+
return fn(dispose);
|
|
338
|
+
} finally {
|
|
339
|
+
currentRoot = prevRoot;
|
|
340
|
+
}
|
|
341
|
+
}
|