what-core 0.6.2 → 0.7.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/index.js +312 -182
- package/dist/index.js.map +3 -3
- package/dist/index.min.js +6 -6
- package/dist/index.min.js.map +3 -3
- package/dist/jsx-dev-runtime.js +37 -16
- package/dist/jsx-dev-runtime.js.map +2 -2
- package/dist/jsx-dev-runtime.min.js +1 -1
- package/dist/jsx-dev-runtime.min.js.map +3 -3
- package/dist/jsx-runtime.js +37 -16
- package/dist/jsx-runtime.js.map +2 -2
- package/dist/jsx-runtime.min.js +1 -1
- package/dist/jsx-runtime.min.js.map +3 -3
- package/dist/render.js +256 -194
- package/dist/render.js.map +3 -3
- package/dist/render.min.js +1 -1
- package/dist/render.min.js.map +3 -3
- package/dist/testing.js +174 -149
- package/dist/testing.js.map +2 -2
- package/dist/testing.min.js +1 -1
- package/dist/testing.min.js.map +3 -3
- package/package.json +1 -1
- package/src/dom.js +78 -47
- package/src/h.js +43 -18
- package/src/reactive.js +176 -101
- package/src/render.js +118 -34
package/src/reactive.js
CHANGED
|
@@ -29,9 +29,9 @@ let batchDepth = 0;
|
|
|
29
29
|
let pendingEffects = [];
|
|
30
30
|
let pendingNeedSort = false; // Track whether pendingEffects actually needs sorting
|
|
31
31
|
|
|
32
|
-
// WeakMap
|
|
33
|
-
//
|
|
34
|
-
|
|
32
|
+
// Instead of a WeakMap from subscriber Set → owning computed's inner effect,
|
|
33
|
+
// we store the owner directly on the Set as ._owner (20x faster than WeakMap.get).
|
|
34
|
+
// Signal subscriber Sets have ._owner = undefined (signals are level 0).
|
|
35
35
|
|
|
36
36
|
// --- Iterative Computed Evaluation State ---
|
|
37
37
|
// Uses a throw/catch trampoline to convert recursive computed evaluation
|
|
@@ -43,22 +43,28 @@ let iterativeEvalStack = null; // array when inside evaluation loop, null other
|
|
|
43
43
|
// --- Signal ---
|
|
44
44
|
// A reactive value. Reading inside an effect auto-tracks the dependency.
|
|
45
45
|
// Writing triggers only the effects that depend on this signal.
|
|
46
|
+
//
|
|
47
|
+
// Performance: signal read is the hottest path in any signal-based framework.
|
|
48
|
+
// Key optimizations:
|
|
49
|
+
// - No rest args (...args) — uses arguments.length for zero-alloc read path
|
|
50
|
+
// - Subscriber tracking uses lastTracked to skip redundant Set.add/Array.push
|
|
51
|
+
// when the same signal is read multiple times in one effect (common pattern)
|
|
52
|
+
// - Write path uses === first (fast for primitives), falls back to Object.is
|
|
53
|
+
// only for NaN detection
|
|
54
|
+
// - subs.size check avoids notify() call when no subscribers
|
|
46
55
|
|
|
47
56
|
export function signal(initial, debugName) {
|
|
48
57
|
let value = initial;
|
|
49
58
|
const subs = new Set();
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return value;
|
|
60
|
-
}
|
|
61
|
-
// Write
|
|
59
|
+
// Track the last effect that subscribed — skip redundant tracking when the
|
|
60
|
+
// same effect reads this signal multiple times (common in template bindings).
|
|
61
|
+
// lastTrackedEpoch tracks the effect's cleanup epoch to detect stale caches.
|
|
62
|
+
let lastTracked = null;
|
|
63
|
+
let lastTrackedEpoch = 0;
|
|
64
|
+
|
|
65
|
+
// Shared write logic — inlined via _sigWrite closure to avoid per-call overhead
|
|
66
|
+
// while keeping the sig() function body minimal for V8 optimization.
|
|
67
|
+
function _sigWrite(next) {
|
|
62
68
|
if (__DEV__ && insideComputed) {
|
|
63
69
|
console.warn(
|
|
64
70
|
'[what] Signal.set() called inside a computed function. ' +
|
|
@@ -66,27 +72,42 @@ export function signal(initial, debugName) {
|
|
|
66
72
|
(debugName ? ` (signal: ${debugName})` : '')
|
|
67
73
|
);
|
|
68
74
|
}
|
|
69
|
-
const nextVal = typeof
|
|
70
|
-
|
|
75
|
+
const nextVal = typeof next === 'function' ? next(value) : next;
|
|
76
|
+
// Fast equality: === handles all primitives except NaN.
|
|
77
|
+
// Only fall through for the NaN !== NaN case.
|
|
78
|
+
if (value === nextVal || (value !== value && nextVal !== nextVal)) return;
|
|
71
79
|
value = nextVal;
|
|
80
|
+
// Invalidate lastTracked since value changed — any effect that reads
|
|
81
|
+
// this signal during re-run needs to re-track.
|
|
82
|
+
lastTracked = null;
|
|
72
83
|
if (__DEV__ && __devtools) __devtools.onSignalUpdate(sig);
|
|
73
84
|
if (subs.size > 0) notify(subs);
|
|
74
85
|
}
|
|
75
86
|
|
|
76
|
-
sig
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
)
|
|
87
|
+
// Unified getter/setter: sig() reads, sig(newVal) writes
|
|
88
|
+
// Using arguments.length instead of rest args avoids array allocation on read
|
|
89
|
+
function sig(newVal) {
|
|
90
|
+
if (arguments.length === 0) {
|
|
91
|
+
// Read — hot path, keep minimal
|
|
92
|
+
const ce = currentEffect;
|
|
93
|
+
if (ce !== null) {
|
|
94
|
+
// Only track if this signal isn't already in the effect's deps.
|
|
95
|
+
// lastTracked is a fast cache for the common case (single effect reading
|
|
96
|
+
// this signal). It's reset to null on write and on cleanup epoch change.
|
|
97
|
+
if (ce !== lastTracked || ce._epoch !== lastTrackedEpoch) {
|
|
98
|
+
lastTracked = ce;
|
|
99
|
+
lastTrackedEpoch = ce._epoch;
|
|
100
|
+
subs.add(ce);
|
|
101
|
+
ce.deps.push(subs);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
83
105
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
};
|
|
106
|
+
// Write via sig(newVal)
|
|
107
|
+
_sigWrite(newVal);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
sig.set = _sigWrite;
|
|
90
111
|
|
|
91
112
|
sig.peek = () => value;
|
|
92
113
|
|
|
@@ -113,6 +134,8 @@ export function signal(initial, debugName) {
|
|
|
113
134
|
export function computed(fn) {
|
|
114
135
|
let value, dirty = true;
|
|
115
136
|
const subs = new Set();
|
|
137
|
+
let lastTracked = null;
|
|
138
|
+
let lastTrackedEpoch = 0;
|
|
116
139
|
|
|
117
140
|
const inner = _createEffect(() => {
|
|
118
141
|
const prevInsideComputed = insideComputed;
|
|
@@ -131,16 +154,21 @@ export function computed(fn) {
|
|
|
131
154
|
inner._computedSubs = subs;
|
|
132
155
|
|
|
133
156
|
// Register this subscriber set as owned by this computed
|
|
134
|
-
|
|
157
|
+
subs._owner = inner;
|
|
135
158
|
|
|
136
159
|
// Store markDirty/isDirty closures on the inner effect for iterative eval
|
|
137
160
|
inner._markDirty = () => { dirty = true; };
|
|
138
161
|
inner._isDirty = () => dirty;
|
|
139
162
|
|
|
140
163
|
function read() {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
164
|
+
const ce = currentEffect;
|
|
165
|
+
if (ce !== null) {
|
|
166
|
+
if (ce !== lastTracked || ce._epoch !== lastTrackedEpoch) {
|
|
167
|
+
lastTracked = ce;
|
|
168
|
+
lastTrackedEpoch = ce._epoch;
|
|
169
|
+
subs.add(ce);
|
|
170
|
+
ce.deps.push(subs);
|
|
171
|
+
}
|
|
144
172
|
}
|
|
145
173
|
if (dirty) _evaluateComputed(inner);
|
|
146
174
|
return value;
|
|
@@ -149,6 +177,7 @@ export function computed(fn) {
|
|
|
149
177
|
// When a dependency changes, mark dirty AND propagate to our subscribers.
|
|
150
178
|
inner._onNotify = () => {
|
|
151
179
|
dirty = true;
|
|
180
|
+
lastTracked = null; // Invalidate tracking cache on value change
|
|
152
181
|
if (subs.size > 0) notify(subs);
|
|
153
182
|
};
|
|
154
183
|
|
|
@@ -203,7 +232,7 @@ function _evaluateComputed(computedEffect) {
|
|
|
203
232
|
let pushedUpstream = false;
|
|
204
233
|
const deps = current.deps;
|
|
205
234
|
for (let i = 0; i < deps.length; i++) {
|
|
206
|
-
const depOwner =
|
|
235
|
+
const depOwner = deps[i]._owner;
|
|
207
236
|
if (depOwner && depOwner._computed && depOwner._isDirty && depOwner._isDirty()) {
|
|
208
237
|
stack.push(depOwner);
|
|
209
238
|
pushedUpstream = true;
|
|
@@ -245,7 +274,7 @@ function _updateLevel(e) {
|
|
|
245
274
|
let maxDepLevel = 0;
|
|
246
275
|
const deps = e.deps;
|
|
247
276
|
for (let i = 0; i < deps.length; i++) {
|
|
248
|
-
const owner =
|
|
277
|
+
const owner = deps[i]._owner;
|
|
249
278
|
if (owner) {
|
|
250
279
|
const depLevel = owner._level;
|
|
251
280
|
if (depLevel > maxDepLevel) maxDepLevel = depLevel;
|
|
@@ -299,7 +328,9 @@ export function batch(fn) {
|
|
|
299
328
|
|
|
300
329
|
function _createEffect(fn, lazy) {
|
|
301
330
|
// Minimal object shape — computed() adds extra properties after creation.
|
|
302
|
-
//
|
|
331
|
+
// IMPORTANT: V8 optimizes objects with a consistent "hidden class" (shape).
|
|
332
|
+
// All properties must be declared upfront even if null — adding properties
|
|
333
|
+
// later causes shape transitions which deoptimize property access globally.
|
|
303
334
|
const e = {
|
|
304
335
|
fn,
|
|
305
336
|
deps: [], // array of subscriber sets (cheaper than Set for typical 1-3 deps)
|
|
@@ -313,6 +344,8 @@ function _createEffect(fn, lazy) {
|
|
|
313
344
|
_computedSubs: null, // reference to the computed's subscriber set
|
|
314
345
|
_isDirty: null, // function to check if computed is dirty (set by computed())
|
|
315
346
|
_markDirty: null, // function to mark computed dirty (set by computed())
|
|
347
|
+
_cleanup: null, // cleanup function returned by effect fn (declared upfront for shape)
|
|
348
|
+
_epoch: 0, // incremented on cleanup — used by signal lastTracked cache
|
|
316
349
|
};
|
|
317
350
|
if (__DEV__ && __devtools) __devtools.onEffectCreate(e);
|
|
318
351
|
return e;
|
|
@@ -322,6 +355,9 @@ function _runEffect(e) {
|
|
|
322
355
|
if (e.disposed) return;
|
|
323
356
|
|
|
324
357
|
// Stable effect fast path: deps don't change, skip cleanup/re-subscribe.
|
|
358
|
+
// This is critical for performance: effects like `() => el.className = sig() ? 'a' : ''`
|
|
359
|
+
// always read the same signal(s). After auto-promotion, re-runs skip the O(deps)
|
|
360
|
+
// cleanup + re-subscribe cycle entirely.
|
|
325
361
|
if (e._stable) {
|
|
326
362
|
if (e._cleanup) {
|
|
327
363
|
try { e._cleanup(); } catch (err) {
|
|
@@ -344,11 +380,15 @@ function _runEffect(e) {
|
|
|
344
380
|
return;
|
|
345
381
|
}
|
|
346
382
|
|
|
383
|
+
// Save the single dep for auto-stable detection (safe: 1-dep effects
|
|
384
|
+
// have deterministic dep sets — no conditional reads possible).
|
|
385
|
+
const singleDep = e.deps.length === 1 ? e.deps[0] : null;
|
|
386
|
+
|
|
347
387
|
cleanup(e);
|
|
348
388
|
// Run effect cleanup from previous run
|
|
349
389
|
if (e._cleanup) {
|
|
350
390
|
try { e._cleanup(); } catch (err) {
|
|
351
|
-
if (__devtools?.onError) __devtools.onError(err, { type: 'effect-cleanup', effect: e });
|
|
391
|
+
if (__DEV__ && __devtools?.onError) __devtools.onError(err, { type: 'effect-cleanup', effect: e });
|
|
352
392
|
if (__DEV__) console.warn('[what] Error in effect cleanup:', err);
|
|
353
393
|
}
|
|
354
394
|
e._cleanup = null;
|
|
@@ -363,11 +403,23 @@ function _runEffect(e) {
|
|
|
363
403
|
}
|
|
364
404
|
} catch (err) {
|
|
365
405
|
if (err === NEEDS_UPSTREAM) throw err; // Iterative eval sentinel — not a real error
|
|
366
|
-
if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
406
|
+
if (__DEV__ && __devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
367
407
|
throw err;
|
|
368
408
|
} finally {
|
|
369
409
|
currentEffect = prev;
|
|
370
410
|
}
|
|
411
|
+
|
|
412
|
+
// Auto-promote to stable: effects with exactly 1 dep that remains the same
|
|
413
|
+
// after re-run have a fixed dependency graph. Skip cleanup/re-subscribe
|
|
414
|
+
// on future re-runs. This is safe because a single-dep effect can't have
|
|
415
|
+
// conditional signal reads that change which signal is tracked.
|
|
416
|
+
// Guard: don't promote self-triggering effects (those that write to the signal
|
|
417
|
+
// they read, causing re-queuing). Check e._pending to detect this.
|
|
418
|
+
if (singleDep !== null && e.deps.length === 1 && e.deps[0] === singleDep
|
|
419
|
+
&& !e._cleanup && !e._pending) {
|
|
420
|
+
e._stable = true;
|
|
421
|
+
}
|
|
422
|
+
|
|
371
423
|
if (__DEV__ && __devtools?.onEffectRun) __devtools.onEffectRun(e);
|
|
372
424
|
}
|
|
373
425
|
|
|
@@ -388,6 +440,9 @@ function cleanup(e) {
|
|
|
388
440
|
const deps = e.deps;
|
|
389
441
|
for (let i = 0; i < deps.length; i++) deps[i].delete(e);
|
|
390
442
|
deps.length = 0;
|
|
443
|
+
// Increment epoch so signals' lastTracked cache is invalidated.
|
|
444
|
+
// This ensures a signal will re-track this effect after cleanup.
|
|
445
|
+
e._epoch++;
|
|
391
446
|
}
|
|
392
447
|
|
|
393
448
|
// --- Notification ---
|
|
@@ -400,6 +455,43 @@ let notifyDepth = 0; // Tracks recursive notify depth
|
|
|
400
455
|
let notifyQueue = null; // Reusable queue, allocated on first recursive call
|
|
401
456
|
let notifyQueueLen = 0; // Length of the queue
|
|
402
457
|
|
|
458
|
+
// Process a single subscriber during notification.
|
|
459
|
+
// Extracted to avoid code duplication between outer and queue drain paths.
|
|
460
|
+
function _processSubscriber(e) {
|
|
461
|
+
if (e.disposed) return;
|
|
462
|
+
if (e._onNotify) {
|
|
463
|
+
// Computed subscriber: mark dirty and propagate.
|
|
464
|
+
// _onNotify may call notify() recursively — tracked by notifyDepth.
|
|
465
|
+
e._onNotify();
|
|
466
|
+
} else if (!e._pending) {
|
|
467
|
+
if (batchDepth === 0 && e._stable) {
|
|
468
|
+
// Inline execution for stable effects — no pending queue needed
|
|
469
|
+
const prev = currentEffect;
|
|
470
|
+
currentEffect = null;
|
|
471
|
+
try {
|
|
472
|
+
const result = e.fn();
|
|
473
|
+
if (typeof result === 'function') {
|
|
474
|
+
if (e._cleanup) try { e._cleanup(); } catch (err) { /* ignore */ }
|
|
475
|
+
e._cleanup = result;
|
|
476
|
+
}
|
|
477
|
+
} catch (err) {
|
|
478
|
+
if (__DEV__ && __devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
479
|
+
if (__DEV__) console.warn('[what] Error in stable effect:', err);
|
|
480
|
+
} finally {
|
|
481
|
+
currentEffect = prev;
|
|
482
|
+
}
|
|
483
|
+
} else {
|
|
484
|
+
e._pending = true;
|
|
485
|
+
const level = e._level;
|
|
486
|
+
const len = pendingEffects.length;
|
|
487
|
+
if (len > 0 && pendingEffects[len - 1]._level > level) {
|
|
488
|
+
pendingNeedSort = true;
|
|
489
|
+
}
|
|
490
|
+
pendingEffects.push(e);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
403
495
|
function notify(subs) {
|
|
404
496
|
// Fast path: no recursive notifications in progress — iterate directly.
|
|
405
497
|
// This avoids array allocation for the common case (signal → effects).
|
|
@@ -407,36 +499,7 @@ function notify(subs) {
|
|
|
407
499
|
notifyDepth = 1;
|
|
408
500
|
try {
|
|
409
501
|
for (const e of subs) {
|
|
410
|
-
|
|
411
|
-
if (e._onNotify) {
|
|
412
|
-
// Computed subscriber: mark dirty and propagate.
|
|
413
|
-
// _onNotify may call notify() recursively — tracked by notifyDepth.
|
|
414
|
-
e._onNotify();
|
|
415
|
-
} else if (batchDepth === 0 && e._stable) {
|
|
416
|
-
// Inline execution for stable effects
|
|
417
|
-
const prev = currentEffect;
|
|
418
|
-
currentEffect = null;
|
|
419
|
-
try {
|
|
420
|
-
const result = e.fn();
|
|
421
|
-
if (typeof result === 'function') {
|
|
422
|
-
if (e._cleanup) try { e._cleanup(); } catch (err) {}
|
|
423
|
-
e._cleanup = result;
|
|
424
|
-
}
|
|
425
|
-
} catch (err) {
|
|
426
|
-
if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
427
|
-
if (__DEV__) console.warn('[what] Error in stable effect:', err);
|
|
428
|
-
} finally {
|
|
429
|
-
currentEffect = prev;
|
|
430
|
-
}
|
|
431
|
-
} else if (!e._pending) {
|
|
432
|
-
e._pending = true;
|
|
433
|
-
const level = e._level;
|
|
434
|
-
const len = pendingEffects.length;
|
|
435
|
-
if (len > 0 && pendingEffects[len - 1]._level > level) {
|
|
436
|
-
pendingNeedSort = true;
|
|
437
|
-
}
|
|
438
|
-
pendingEffects.push(e);
|
|
439
|
-
}
|
|
502
|
+
_processSubscriber(e);
|
|
440
503
|
}
|
|
441
504
|
// Drain any queued subscriber sets from recursive notify calls
|
|
442
505
|
if (notifyQueueLen > 0) {
|
|
@@ -446,33 +509,7 @@ function notify(subs) {
|
|
|
446
509
|
notifyQueue[qi] = null; // Allow GC
|
|
447
510
|
qi++;
|
|
448
511
|
for (const e of queuedSubs) {
|
|
449
|
-
|
|
450
|
-
if (e._onNotify) {
|
|
451
|
-
e._onNotify();
|
|
452
|
-
} else if (batchDepth === 0 && e._stable) {
|
|
453
|
-
const prev = currentEffect;
|
|
454
|
-
currentEffect = null;
|
|
455
|
-
try {
|
|
456
|
-
const result = e.fn();
|
|
457
|
-
if (typeof result === 'function') {
|
|
458
|
-
if (e._cleanup) try { e._cleanup(); } catch (err) {}
|
|
459
|
-
e._cleanup = result;
|
|
460
|
-
}
|
|
461
|
-
} catch (err) {
|
|
462
|
-
if (__devtools?.onError) __devtools.onError(err, { type: 'effect', effect: e });
|
|
463
|
-
if (__DEV__) console.warn('[what] Error in stable effect:', err);
|
|
464
|
-
} finally {
|
|
465
|
-
currentEffect = prev;
|
|
466
|
-
}
|
|
467
|
-
} else if (!e._pending) {
|
|
468
|
-
e._pending = true;
|
|
469
|
-
const level = e._level;
|
|
470
|
-
const len = pendingEffects.length;
|
|
471
|
-
if (len > 0 && pendingEffects[len - 1]._level > level) {
|
|
472
|
-
pendingNeedSort = true;
|
|
473
|
-
}
|
|
474
|
-
pendingEffects.push(e);
|
|
475
|
-
}
|
|
512
|
+
_processSubscriber(e);
|
|
476
513
|
}
|
|
477
514
|
}
|
|
478
515
|
notifyQueueLen = 0;
|
|
@@ -544,10 +581,6 @@ function flush() {
|
|
|
544
581
|
iterations++;
|
|
545
582
|
}
|
|
546
583
|
if (iterations >= 25) {
|
|
547
|
-
// Clear pending effects to prevent further damage
|
|
548
|
-
for (let i = 0; i < pendingEffects.length; i++) pendingEffects[i]._pending = false;
|
|
549
|
-
pendingEffects.length = 0;
|
|
550
|
-
|
|
551
584
|
if (__DEV__) {
|
|
552
585
|
const remaining = pendingEffects.slice(0, 3);
|
|
553
586
|
const effectNames = remaining.map(e => e.fn?.name || e.fn?.toString().slice(0, 60) || '(anonymous)');
|
|
@@ -560,6 +593,9 @@ function flush() {
|
|
|
560
593
|
} else {
|
|
561
594
|
console.warn('[what] Possible infinite effect loop detected');
|
|
562
595
|
}
|
|
596
|
+
// Clear pending effects AFTER capturing debug info
|
|
597
|
+
for (let i = 0; i < pendingEffects.length; i++) pendingEffects[i]._pending = false;
|
|
598
|
+
pendingEffects.length = 0;
|
|
563
599
|
}
|
|
564
600
|
} finally {
|
|
565
601
|
isFlushing = false;
|
|
@@ -605,7 +641,7 @@ export function memo(fn) {
|
|
|
605
641
|
_updateLevel(e);
|
|
606
642
|
|
|
607
643
|
// Register subscriber set owner for level tracking
|
|
608
|
-
|
|
644
|
+
subs._owner = e;
|
|
609
645
|
|
|
610
646
|
// Register with current root
|
|
611
647
|
if (currentRoot) {
|
|
@@ -760,6 +796,45 @@ function _disposeRoot(root) {
|
|
|
760
796
|
root.disposals.length = 0;
|
|
761
797
|
}
|
|
762
798
|
|
|
799
|
+
// --- _createItemScope ---
|
|
800
|
+
// Lightweight reactive scope for list items. Unlike createRoot, this does NOT
|
|
801
|
+
// register with the parent ownership tree (saves ~40% allocation overhead).
|
|
802
|
+
// Used by mapArray where disposal is managed explicitly by the list reconciler.
|
|
803
|
+
export function _createItemScope(fn) {
|
|
804
|
+
const prevRoot = currentRoot;
|
|
805
|
+
const prevOwner = currentOwner;
|
|
806
|
+
const scope = {
|
|
807
|
+
disposals: [],
|
|
808
|
+
owner: null, // No parent registration
|
|
809
|
+
children: [], // Kept for compat with effects that create sub-roots
|
|
810
|
+
_disposed: false,
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
currentRoot = scope;
|
|
814
|
+
currentOwner = scope;
|
|
815
|
+
|
|
816
|
+
try {
|
|
817
|
+
const dispose = () => {
|
|
818
|
+
if (scope._disposed) return;
|
|
819
|
+
scope._disposed = true;
|
|
820
|
+
// Dispose children
|
|
821
|
+
for (let i = scope.children.length - 1; i >= 0; i--) {
|
|
822
|
+
_disposeRoot(scope.children[i]);
|
|
823
|
+
}
|
|
824
|
+
scope.children.length = 0;
|
|
825
|
+
// Dispose own effects
|
|
826
|
+
for (let i = scope.disposals.length - 1; i >= 0; i--) {
|
|
827
|
+
scope.disposals[i]();
|
|
828
|
+
}
|
|
829
|
+
scope.disposals.length = 0;
|
|
830
|
+
};
|
|
831
|
+
return fn(dispose);
|
|
832
|
+
} finally {
|
|
833
|
+
currentRoot = prevRoot;
|
|
834
|
+
currentOwner = prevOwner;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
763
838
|
// --- onCleanup ---
|
|
764
839
|
// Register a cleanup function with the current owner/root.
|
|
765
840
|
// Runs when the owner is disposed.
|