diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 7f3d7f1dba..794a04ebb8 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -1,4 +1,3 @@ -export const SOURCE = 1; export const DERIVED = 1 << 1; export const EFFECT = 1 << 2; export const PRE_EFFECT = 1 << 3; diff --git a/packages/svelte/src/internal/client/custom-element.js b/packages/svelte/src/internal/client/custom-element.js index 0bd64cdf50..2d5a8fe08c 100644 --- a/packages/svelte/src/internal/client/custom-element.js +++ b/packages/svelte/src/internal/client/custom-element.js @@ -1,6 +1,5 @@ import { createClassComponent } from '../../legacy/legacy-client.js'; -import { destroy_signal } from './runtime.js'; -import { render_effect } from './reactivity/effects.js'; +import { destroy_effect, render_effect } from './reactivity/effects.js'; import { open, close } from './render.js'; import { define_property } from './utils.js'; @@ -199,7 +198,7 @@ if (typeof HTMLElement === 'function') { Promise.resolve().then(() => { if (!this.$$cn) { this.$$c.$destroy(); - destroy_signal(this.$$me); + destroy_effect(this.$$me); this.$$c = undefined; } }); diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 4f24a4d4ae..f57249df1f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -1,14 +1,8 @@ import { is_promise } from '../../../common.js'; import { hydrate_block_anchor } from '../../hydration.js'; import { remove } from '../../reconciler.js'; -import { - current_block, - destroy_signal, - execute_effect, - flushSync, - push_destroy_fn -} from '../../runtime.js'; -import { render_effect } from '../../reactivity/effects.js'; +import { current_block, execute_effect, flushSync } from '../../runtime.js'; +import { destroy_effect, render_effect } from '../../reactivity/effects.js'; import { trigger_transitions } from '../../transitions.js'; import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js'; @@ -74,7 +68,7 @@ export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) { remove(render.d); render.d = null; } - destroy_signal(render.e); + destroy_effect(render.e); render.e = null; } } @@ -181,7 +175,7 @@ export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) { block, false ); - push_destroy_fn(await_effect, () => { + await_effect.ondestroy = () => { let render = current_render; latest_token = {}; while (render !== null) { @@ -191,10 +185,10 @@ export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) { } const effect = render.e; if (effect !== null) { - destroy_signal(effect); + destroy_effect(effect); } render = render.p; } - }); + }; block.e = await_effect; } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 93899a016d..5a572a9729 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -16,8 +16,8 @@ import { } from '../../hydration.js'; import { clear_text_content, empty, map_get, map_set } from '../../operations.js'; import { insert, remove } from '../../reconciler.js'; -import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js'; -import { render_effect } from '../../reactivity/effects.js'; +import { current_block, execute_effect } from '../../runtime.js'; +import { destroy_effect, render_effect } from '../../reactivity/effects.js'; import { source, mutable_source, set } from '../../reactivity/sources.js'; import { trigger_transitions } from '../../transitions.js'; import { is_array, is_frozen } from '../../utils.js'; @@ -91,7 +91,7 @@ export function create_each_item_block(item, index, key) { * @param {() => V[]} collection * @param {number} flags * @param {null | ((item: V) => string)} key_fn - * @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal) => void} render_fn + * @param {(anchor: null, item: V, index: import('#client').MaybeSource) => void} render_fn * @param {null | ((anchor: Node) => void)} fallback_fn * @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn * @returns {void} @@ -133,7 +133,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re remove(fallback.d); fallback.d = null; } - destroy_signal(fallback.e); + destroy_effect(fallback.e); fallback.e = null; } } @@ -261,7 +261,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re set_current_hydration_fragment([]); } - push_destroy_fn(each, () => { + each.ondestroy = () => { const flags = block.f; const anchor_node = block.a; const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0; @@ -273,14 +273,14 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re } const effect = fallback.e; if (effect !== null) { - destroy_signal(effect); + destroy_effect(effect); } fallback = fallback.p; } // Clear the array reconcile_fn([], block, anchor_node, is_controlled, render_fn, flags, false, keys); - destroy_signal(/** @type {import('../../types.js').Effect} */ (render)); - }); + destroy_effect(/** @type {import('#client').Effect} */ (render)); + }; block.e = each; } @@ -291,7 +291,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re * @param {() => V[]} collection * @param {number} flags * @param {null | ((item: V) => string)} key_fn - * @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal) => void} render_fn + * @param {(anchor: null, item: V, index: import('#client').MaybeSource) => void} render_fn * @param {null | ((anchor: Node) => void)} fallback_fn * @returns {void} */ @@ -304,7 +304,7 @@ export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fa * @param {Element | Comment} anchor_node * @param {() => V[]} collection * @param {number} flags - * @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal) => void} render_fn + * @param {(anchor: null, item: V, index: import('#client').MaybeSource) => void} render_fn * @param {null | ((anchor: Node) => void)} fallback_fn * @returns {void} */ @@ -904,7 +904,7 @@ export function destroy_each_item_block( if (!controlled && dom !== null) { remove(dom); } - destroy_signal(/** @type {import('../../types.js').Effect} */ (block.e)); + destroy_effect(/** @type {import('#client').Effect} */ (block.e)); } /** diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 57fad94171..5fb43fd0ed 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -6,8 +6,8 @@ import { set_current_hydration_fragment } from '../../hydration.js'; import { remove } from '../../reconciler.js'; -import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js'; -import { render_effect } from '../../reactivity/effects.js'; +import { current_block, execute_effect } from '../../runtime.js'; +import { destroy_effect, render_effect } from '../../reactivity/effects.js'; import { trigger_transitions } from '../../transitions.js'; /** @returns {import('../../types.js').IfBlock} */ @@ -167,15 +167,15 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn) true ); block.ae = alternate_effect; - push_destroy_fn(if_effect, () => { + if_effect.ondestroy = () => { if (consequent_dom !== null) { remove(consequent_dom); } if (alternate_dom !== null) { remove(alternate_dom); } - destroy_signal(consequent_effect); - destroy_signal(alternate_effect); - }); + destroy_effect(consequent_effect); + destroy_effect(alternate_effect); + }; block.e = if_effect; } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index d4cfd175b7..1ac78accd1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,8 +1,8 @@ import { UNINITIALIZED, KEY_BLOCK } from '../../constants.js'; import { hydrate_block_anchor } from '../../hydration.js'; import { remove } from '../../reconciler.js'; -import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js'; -import { render_effect } from '../../reactivity/effects.js'; +import { current_block, execute_effect } from '../../runtime.js'; +import { destroy_effect, render_effect } from '../../reactivity/effects.js'; import { trigger_transitions } from '../../transitions.js'; import { safe_not_equal } from '../../reactivity/equality.js'; @@ -58,7 +58,7 @@ export function key_block(anchor_node, key, render_fn) { remove(render.d); render.d = null; } - destroy_signal(render.e); + destroy_effect(render.e); render.e = null; } } @@ -122,7 +122,7 @@ export function key_block(anchor_node, key, render_fn) { // we trigger the effect after. render(); mounted = true; - push_destroy_fn(key_effect, () => { + key_effect.ondestroy = () => { let render = current_render; while (render !== null) { const dom = render.d; @@ -131,10 +131,10 @@ export function key_block(anchor_node, key, render_fn) { } const effect = render.e; if (effect !== null) { - destroy_signal(effect); + destroy_effect(effect); } render = render.p; } - }); + }; block.e = key_effect; } diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 0acfc8b5f4..fbc827cee5 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,11 +1,5 @@ import { DEV } from 'esm-env'; -import { - get, - updating_derived, - batch_inspect, - current_component_context, - untrack -} from './runtime.js'; +import { get, batch_inspect, current_component_context, untrack } from './runtime.js'; import { effect_active } from './reactivity/effects.js'; import { array_prototype, @@ -20,6 +14,7 @@ import { import { add_owner, check_ownership, strip_owner } from './dev/ownership.js'; import { mutable_source, source, set } from './reactivity/sources.js'; import { STATE_SYMBOL, UNINITIALIZED } from './constants.js'; +import { updating_derived } from './reactivity/deriveds.js'; /** * @template T diff --git a/packages/svelte/src/internal/client/reactivity/deriveds.js b/packages/svelte/src/internal/client/reactivity/deriveds.js index ef59b4b940..24806084ae 100644 --- a/packages/svelte/src/internal/client/reactivity/deriveds.js +++ b/packages/svelte/src/internal/client/reactivity/deriveds.js @@ -1,41 +1,52 @@ import { DEV } from 'esm-env'; -import { CLEAN, DERIVED, UNINITIALIZED, UNOWNED } from '../constants.js'; -import { current_block, current_consumer, current_effect } from '../runtime.js'; -import { push_reference } from './effects.js'; -import { default_equals, safe_equal } from './equality.js'; +import { CLEAN, DERIVED, DESTROYED, DIRTY, MAYBE_DIRTY, UNOWNED } from '../constants.js'; +import { + current_reaction, + current_effect, + destroy_children, + remove_reactions, + set_signal_status, + mark_reactions, + current_skip_reaction, + execute_reaction_fn +} from '../runtime.js'; +import { equals, safe_equals } from './equality.js'; + +export let updating_derived = false; /** * @template V * @param {() => V} fn - * @returns {import('../types.js').Derived} + * @returns {import('#client').Derived} */ /*#__NO_SIDE_EFFECTS__*/ export function derived(fn) { - let flags = DERIVED | CLEAN; + let flags = DERIVED | DIRTY; if (current_effect === null) flags |= UNOWNED; /** @type {import('#client').Derived} */ const signal = { - b: current_block, - c: null, - d: null, - e: default_equals, + reactions: null, + deps: null, + equals, f: flags, - i: fn, - r: null, - // @ts-expect-error - v: UNINITIALIZED, - w: 0, - x: null, - y: null + fn, + effects: null, + deriveds: null, + v: /** @type {V} */ (null), + version: 0 }; if (DEV) { /** @type {import('#client').DerivedDebug} */ (signal).inspect = new Set(); } - if (current_consumer !== null) { - push_reference(current_consumer, signal); + if (current_reaction !== null) { + if (current_reaction.deriveds === null) { + current_reaction.deriveds = [signal]; + } else { + current_reaction.deriveds.push(signal); + } } return signal; @@ -49,6 +60,52 @@ export function derived(fn) { /*#__NO_SIDE_EFFECTS__*/ export function derived_safe_equal(fn) { const signal = derived(fn); - signal.e = safe_equal; + signal.equals = safe_equals; return signal; } + +/** + * @param {import('#client').Derived} derived + * @param {boolean} force_schedule + * @returns {void} + */ +export function update_derived(derived, force_schedule) { + var previous_updating_derived = updating_derived; + updating_derived = true; + destroy_children(derived); + var value = execute_reaction_fn(derived); + updating_derived = previous_updating_derived; + + var status = + (current_skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null + ? MAYBE_DIRTY + : CLEAN; + + set_signal_status(derived, status); + + if (!derived.equals(value)) { + derived.v = value; + mark_reactions(derived, DIRTY, force_schedule); + + if (DEV && force_schedule) { + for (var fn of /** @type {import('#client').DerivedDebug} */ (derived).inspect) fn(); + } + } +} + +/** + * @param {import('#client').Derived} signal + * @returns {void} + */ +export function destroy_derived(signal) { + destroy_children(signal); + remove_reactions(signal, 0); + set_signal_status(signal, DESTROYED); + + signal.effects = + signal.deps = + signal.reactions = + // @ts-expect-error `signal.fn` cannot be `null` while the signal is alive + signal.fn = + null; +} diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 0c9da24f94..8875e03ded 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -3,63 +3,56 @@ import { current_block, current_component_context, current_effect, - destroy_signal, + current_reaction, + destroy_children, flush_local_render_effects, get, - is_runes, + remove_reactions, schedule_effect, + set_signal_status, untrack } from '../runtime.js'; -import { DIRTY, MANAGED, RENDER_EFFECT, EFFECT, PRE_EFFECT } from '../constants.js'; +import { DIRTY, MANAGED, RENDER_EFFECT, EFFECT, PRE_EFFECT, DESTROYED } from '../constants.js'; import { set } from './sources.js'; -/** - * @param {import('#client').Reaction} target_signal - * @param {import('#client').Reaction} ref_signal - * @returns {void} - */ -export function push_reference(target_signal, ref_signal) { - const references = target_signal.r; - if (references === null) { - target_signal.r = [ref_signal]; - } else { - references.push(ref_signal); - } -} - /** * @param {import('./types.js').EffectType} type * @param {(() => void | (() => void)) | ((b: import('#client').Block) => void | (() => void))} fn * @param {boolean} sync * @param {null | import('#client').Block} block - * @param {boolean} schedule + * @param {boolean} init * @returns {import('#client').Effect} */ -function create_effect(type, fn, sync, block, schedule) { +function create_effect(type, fn, sync, block = current_block, init = true) { /** @type {import('#client').Effect} */ const signal = { - b: block, - c: null, - d: null, - e: null, + block, + deps: null, f: type | DIRTY, l: 0, - i: fn, - r: null, - v: null, - w: 0, - x: current_component_context, - y: null + fn, + effects: null, + deriveds: null, + teardown: null, + ctx: current_component_context, + ondestroy: null }; if (current_effect !== null) { signal.l = current_effect.l + 1; - if ((type & MANAGED) === 0) { - push_reference(current_effect, signal); + } + + if ((type & MANAGED) === 0) { + if (current_reaction !== null) { + if (current_reaction.effects === null) { + current_reaction.effects = [signal]; + } else { + current_reaction.effects.push(signal); + } } } - if (schedule) { + if (init) { schedule_effect(signal, sync); } @@ -87,20 +80,17 @@ export function user_effect(fn) { ); } - const apply_component_effect_heuristics = + // Non-nested `$effect(...)` in a component should be deferred + // until the component is mounted + const defer = current_effect.f & RENDER_EFFECT && + // TODO do we actually need this? removing them changes nothing current_component_context !== null && !current_component_context.m; - const effect = create_effect( - EFFECT, - fn, - false, - current_block, - !apply_component_effect_heuristics - ); + const effect = create_effect(EFFECT, fn, false, current_block, !defer); - if (apply_component_effect_heuristics) { + if (defer) { const context = /** @type {import('#client').ComponentContext} */ (current_component_context); (context.e ??= []).push(effect); } @@ -116,7 +106,7 @@ export function user_effect(fn) { export function user_root_effect(fn) { const effect = render_effect(fn, current_block, true); return () => { - destroy_signal(effect); + destroy_effect(effect); }; } @@ -125,7 +115,7 @@ export function user_root_effect(fn) { * @returns {import('#client').Effect} */ export function effect(fn) { - return create_effect(EFFECT, fn, false, current_block, true); + return create_effect(EFFECT, fn, false); } /** @@ -133,16 +123,15 @@ export function effect(fn) { * @returns {import('#client').Effect} */ export function managed_effect(fn) { - return create_effect(EFFECT | MANAGED, fn, false, current_block, true); + return create_effect(EFFECT | MANAGED, fn, false); } /** * @param {() => void | (() => void)} fn - * @param {boolean} sync * @returns {import('#client').Effect} */ -export function managed_pre_effect(fn, sync) { - return create_effect(PRE_EFFECT | MANAGED, fn, sync, current_block, true); +export function managed_pre_effect(fn) { + return create_effect(PRE_EFFECT | MANAGED, fn, false); } /** @@ -159,8 +148,9 @@ export function pre_effect(fn) { : '') ); } + const sync = current_effect !== null && (current_effect.f & RENDER_EFFECT) !== 0; - const runes = is_runes(current_component_context); + return create_effect( PRE_EFFECT, () => { @@ -168,9 +158,7 @@ export function pre_effect(fn) { flush_local_render_effects(); return val; }, - sync, - current_block, - true + sync ); } @@ -196,8 +184,6 @@ export function legacy_pre_effect(deps, fn) { set(component_context.l2, true); return untrack(fn); }, - true, - current_block, true ); } @@ -223,7 +209,7 @@ export function legacy_pre_effect_reset() { * @returns {import('#client').Effect} */ export function invalidate_effect(fn) { - return create_effect(PRE_EFFECT, fn, true, current_block, true); + return create_effect(PRE_EFFECT, fn, true); } /** @@ -239,5 +225,19 @@ export function render_effect(fn, block = current_block, managed = false, sync = if (managed) { flags |= MANAGED; } - return create_effect(flags, /** @type {any} */ (fn), sync, block, true); + return create_effect(flags, /** @type {any} */ (fn), sync, block); +} + +/** + * @param {import('#client').Effect} signal + * @returns {void} + */ +export function destroy_effect(signal) { + destroy_children(signal); + remove_reactions(signal, 0); + set_signal_status(signal, DESTROYED); + + signal.teardown?.(); + signal.ondestroy?.(); + signal.fn = signal.effects = signal.ondestroy = signal.ctx = signal.block = signal.deps = null; } diff --git a/packages/svelte/src/internal/client/reactivity/equality.js b/packages/svelte/src/internal/client/reactivity/equality.js index 34673b2dda..0a5749442f 100644 --- a/packages/svelte/src/internal/client/reactivity/equality.js +++ b/packages/svelte/src/internal/client/reactivity/equality.js @@ -1,10 +1,6 @@ -/** - * @param {unknown} a - * @param {unknown} b - * @returns {boolean} - */ -export function default_equals(a, b) { - return a === b; +/** @type {import('#client').Equals} */ +export function equals(value) { + return value === this.v; } /** @@ -20,11 +16,7 @@ export function safe_not_equal(a, b) { : a !== b || (a !== null && typeof a === 'object') || typeof a === 'function'; } -/** - * @param {unknown} a - * @param {unknown} b - * @returns {boolean} - */ -export function safe_equal(a, b) { - return !safe_not_equal(a, b); +/** @type {import('#client').Equals} */ +export function safe_equals(value) { + return !safe_not_equal(value, this.v); } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 504ecb1efe..c35b0a84e0 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -1,7 +1,7 @@ import { DEV } from 'esm-env'; import { current_component_context, - current_consumer, + current_reaction, current_dependencies, current_effect, current_untracked_writes, @@ -11,15 +11,15 @@ import { ignore_mutation_validation, is_batching_effect, is_runes, - mark_signal_consumers, + mark_reactions, schedule_effect, set_current_untracked_writes, set_last_inspected_signal, set_signal_status, untrack } from '../runtime.js'; -import { default_equals, safe_equal } from './equality.js'; -import { CLEAN, DERIVED, DIRTY, MANAGED, SOURCE } from '../constants.js'; +import { equals, safe_equals } from './equality.js'; +import { CLEAN, DERIVED, DIRTY, MANAGED } from '../constants.js'; /** * @template V @@ -30,15 +30,15 @@ import { CLEAN, DERIVED, DIRTY, MANAGED, SOURCE } from '../constants.js'; export function source(value) { /** @type {import('#client').Source} */ const source = { - c: null, - e: default_equals, - f: SOURCE | CLEAN, + f: 0, // TODO ideally we could skip this altogether, but it causes type errors + reactions: null, + equals: equals, v: value, - w: 0 + version: 0 }; if (DEV) { - /** @type {import('#client').SourceDebug} */ (source).inspect = new Set(); + /** @type {import('#client').ValueDebug} */ (source).inspect = new Set(); } return source; @@ -52,7 +52,7 @@ export function source(value) { /*#__NO_SIDE_EFFECTS__*/ export function mutable_source(initial_value) { const s = source(initial_value); - s.e = safe_equal; + s.equals = safe_equals; // bind the signal to the component context, in case we need to // track updates to trigger beforeUpdate/afterUpdate callbacks @@ -96,9 +96,9 @@ export function set(signal, value) { if ( !current_untracking && !ignore_mutation_validation && - current_consumer !== null && - is_runes(null) && - (current_consumer.f & DERIVED) !== 0 + current_reaction !== null && + is_runes() && + (current_reaction.f & DERIVED) !== 0 ) { throw new Error( 'ERR_SVELTE_UNSAFE_MUTATION' + @@ -109,16 +109,16 @@ export function set(signal, value) { : '') ); } - if ( - (signal.f & SOURCE) !== 0 && - !(/** @type {import('#client').EqualsFunctions} */ (signal.e)(value, signal.v)) - ) { + + if (!signal.equals(value)) { signal.v = value; - // Increment write version so that unowned signals can properly track dirtyness - signal.w++; + + // Increment write version so that unowned signals can properly track dirtiness + signal.version++; + // If the current signal is running for the first time, it won't have any - // consumers as we only allocate and assign the consumers after the signal - // has fully executed. So in the case of ensuring it registers the consumer + // reactions as we only allocate and assign the reactions after the signal + // has fully executed. So in the case of ensuring it registers the reaction // properly for itself, we need to ensure the current effect actually gets // scheduled. i.e: // @@ -127,10 +127,9 @@ export function set(signal, value) { // We additionally want to skip this logic for when ignore_mutation_validation is // true, as stores write to source signal on initialisation. if ( - is_runes(null) && + is_runes() && !ignore_mutation_validation && current_effect !== null && - current_effect.c === null && (current_effect.f & CLEAN) !== 0 && (current_effect.f & MANAGED) === 0 ) { @@ -145,10 +144,10 @@ export function set(signal, value) { } } } - mark_signal_consumers(signal, DIRTY, true); - // @ts-expect-error - if (DEV && signal.inspect) { + mark_reactions(signal, DIRTY, true); + + if (DEV) { if (is_batching_effect) { set_last_inspected_signal(/** @type {import('#client').ValueDebug} */ (signal)); } else { diff --git a/packages/svelte/src/internal/client/reactivity/store.js b/packages/svelte/src/internal/client/reactivity/store.js index 0364e5271e..7ea57428da 100644 --- a/packages/svelte/src/internal/client/reactivity/store.js +++ b/packages/svelte/src/internal/client/reactivity/store.js @@ -27,11 +27,6 @@ export function store_get(store, store_name, stores) { value: mutable_source(UNINITIALIZED), unsubscribe: noop }; - // TODO: can we remove this code? it was refactored out when we split up source/comptued signals - // push_destroy_fn(entry.value, () => { - // /** @type {import('#client').StoreReferencesContainer['']} */ (entry).last_value = - // /** @type {import('#client').StoreReferencesContainer['']} */ (entry).value.value; - // }); stores[store_name] = entry; } @@ -110,8 +105,6 @@ export function unsubscribe_on_destroy(stores) { for (store_name in stores) { const ref = stores[store_name]; ref.unsubscribe(); - // TODO: can we remove this code? it was refactored out when we split up source/comptued signals - // destroy_signal(ref.value); } }); } diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index a390d83b03..7006a7a71f 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -1,89 +1,61 @@ -import type { Block, ComponentContext, EqualsFunctions } from '#client'; -import type { DERIVED, EFFECT, PRE_EFFECT, RENDER_EFFECT, SOURCE } from '../constants'; +import type { Block, ComponentContext, Equals } from '#client'; +import type { EFFECT, PRE_EFFECT, RENDER_EFFECT } from '../constants'; -export type SignalFlags = - | typeof SOURCE - | typeof DERIVED - | typeof EFFECT - | typeof PRE_EFFECT - | typeof RENDER_EFFECT; export type EffectType = typeof EFFECT | typeof PRE_EFFECT | typeof RENDER_EFFECT; -export interface Source { - /** consumers: Signals that read from the current signal */ - c: null | Reaction[]; - /** equals: For value equality */ - e: null | EqualsFunctions; - /** flags: The types that the signal represent, as a bitwise value */ - f: SignalFlags; - /** value: The latest value for this signal */ +export interface Signal { + /** Flags bitmask */ + f: number; +} + +export interface Value extends Signal { + /** Signals that read from this signal */ + reactions: null | Reaction[]; + /** Equality function */ + equals: Equals; + /** The latest value for this signal */ v: V; - // write version - w: number; + /** Write version */ + version: number; } -export interface SourceDebug extends Source { - inspect: Set; +export interface Reaction extends Signal { + /** The reaction function */ + fn: null | Function; + /** Signals that this signal reads from */ + deps: null | Value[]; + /** Effects created inside this signal */ + effects: null | Effect[]; + /** Deriveds created inside this signal */ + deriveds: null | Derived[]; } -export interface Derived extends Source { - /** dependencies: Signals that this signal reads from */ - d: null | Value[]; +export interface Derived extends Value, Reaction { /** The derived function */ - i: () => V; - - // TODO get rid of these + fn: () => V; +} - /** references: Anything that a signal owns */ - r: null | Reaction[]; - /** block: The block associated with this effect/computed */ - b: null | Block; - /** context: The associated component if this signal is an effect/computed */ - x: null | ComponentContext; - /** destroy: Thing(s) that need destroying */ - y: null | (() => void) | Array<() => void>; +export interface Effect extends Reaction { + /** The block associated with this effect */ + block: null | Block; + /** The associated component context */ + ctx: null | ComponentContext; + /** Stuff to do when the effect is destroyed */ + ondestroy: null | (() => void); + /** The effect function */ + fn: null | (() => void | (() => void)) | ((b: Block, s: Signal) => void | (() => void)); + /** The teardown function returned from the effect function */ + teardown: null | (() => void); + /** The depth from the root signal, used for ordering render/pre-effects topologically **/ + l: number; } -export interface DerivedDebug extends Derived { +export interface ValueDebug extends Value { inspect: Set; } -export type Effect = { - /** block: The block associated with this effect/computed */ - b: null | Block; - /** consumers: Signals that read from the current signal */ - c: null | Reaction[]; - /** context: The associated component if this signal is an effect/computed */ - x: null | ComponentContext; - /** dependencies: Signals that this signal reads from */ - d: null | Value[]; - /** destroy: Thing(s) that need destroying */ - // TODO simplify this, it is only used in one place - y: null | (() => void) | Array<() => void>; - /** equals: For value equality */ - e: null | EqualsFunctions; - /** The types that the signal represent, as a bitwise value */ - f: SignalFlags; - /** init: The function that we invoke for effects and computeds */ - i: null | (() => void | (() => void)) | ((b: Block, s: Signal) => void | (() => void)); - /** references: Anything that a signal owns */ - r: null | Reaction[]; - /** value: The latest value for this signal, doubles as the teardown for effects */ - v: null | Function; - /** level: the depth from the root signal, used for ordering render/pre-effects topologically **/ - l: number; - /** write version: used for unowned signals to track if their depdendencies are dirty or not **/ - w: number; -}; - -export type Reaction = Derived | Effect; - -export type MaybeSignal = T | Source; - -export type UnwrappedSignal = T extends Value ? U : T; - -export type Value = Source | Derived; +export interface DerivedDebug extends Derived, ValueDebug {} -export type ValueDebug = SourceDebug | DerivedDebug; +export type Source = Value; -export type Signal = Source | Derived | Effect; +export type MaybeSource = T | Source; diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 75c361d7a9..f21d9729b6 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -35,8 +35,6 @@ import { remove } from './reconciler.js'; import { - destroy_signal, - push_destroy_fn, execute_effect, untrack, flush_sync, @@ -55,7 +53,8 @@ import { effect, managed_effect, pre_effect, - user_effect + user_effect, + destroy_effect } from './reactivity/effects.js'; import { current_hydration_fragment, @@ -76,7 +75,7 @@ import { import { run } from '../common.js'; import { bind_transition, trigger_transitions } from './transitions.js'; import { mutable_source, source, set } from './reactivity/sources.js'; -import { safe_equal, safe_not_equal } from './reactivity/equality.js'; +import { safe_equals, safe_not_equal } from './reactivity/equality.js'; import { STATE_SYMBOL } from './constants.js'; /** @type {Set} */ @@ -728,11 +727,11 @@ export function bind_playback_rate(media, get_value, update) { // Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser. // For hydration we could do it immediately but the additional code is not worth the lost microtask. - /** @type {import('./types.js').Reaction | undefined} */ + /** @type {import('./types.js').Effect | undefined} */ let render; let destroyed = false; const effect = managed_effect(() => { - destroy_signal(effect); + destroy_effect(effect); if (destroyed) return; if (get_value() == null) { callback(); @@ -750,7 +749,7 @@ export function bind_playback_rate(media, get_value, update) { render_effect(() => () => { destroyed = true; if (render) { - destroy_signal(render); + destroy_effect(render); } }); } @@ -1382,7 +1381,7 @@ export function bind_this(element_or_component, update, get_value, get_parts) { // Add effect teardown (likely causes: if block became false, each item removed, component unmounted). // In these cases we need to nullify the binding only if we detect that the value is still the same. // If not, that means that another effect has now taken over the binding. - push_destroy_fn(e, () => { + e.ondestroy = () => { // Defer to the next tick so that all updates can be reconciled first. // This solves the case where one variable is shared across multiple this-bindings. effect(() => { @@ -1390,7 +1389,7 @@ export function bind_this(element_or_component, update, get_value, get_parts) { update(null, ...parts); } }); - }); + }; } /** @@ -1553,12 +1552,12 @@ export function head(render_fn) { block, false ); - push_destroy_fn(head_effect, () => { + head_effect.ondestroy = () => { const current = block.d; if (current !== null) { remove(current); } - }); + }; block.e = head_effect; } finally { if (is_hydrating) { @@ -1666,14 +1665,14 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) { block, true ); - push_destroy_fn(element_effect, () => { + element_effect.ondestroy = () => { if (element !== null) { remove(element); block.d = null; element = null; } - destroy_signal(render_effect_signal); - }); + destroy_effect(render_effect_signal); + }; block.e = element_effect; } @@ -1712,7 +1711,7 @@ export function component(anchor_node, component_fn, render_fn) { remove(render.d); render.d = null; } - destroy_signal(render.e); + destroy_effect(render.e); render.e = null; } } @@ -1779,7 +1778,7 @@ export function component(anchor_node, component_fn, render_fn) { block, false ); - push_destroy_fn(component_effect, () => { + component_effect.ondestroy = () => { let render = current_render; while (render !== null) { const dom = render.d; @@ -1788,11 +1787,11 @@ export function component(anchor_node, component_fn, render_fn) { } const effect = render.e; if (effect !== null) { - destroy_signal(effect); + destroy_effect(effect); } render = render.p; } - }); + }; block.e = component_effect; } @@ -1843,9 +1842,9 @@ export function css_props(anchor, is_html, props, component) { } current_props = next_props; }); - push_destroy_fn(effect, () => { + effect.ondestroy = () => { remove(tag); - }); + }; } /** @@ -1875,11 +1874,11 @@ export function html(dom, get_value, svg) { html_dom = reconcile_html(dom, value, svg); } }); - push_destroy_fn(effect, () => { + effect.ondestroy = () => { if (html_dom) { remove(html_dom); } - }); + }; } /** @@ -2659,7 +2658,7 @@ function _mount(Component, options) { if (dom !== null) { remove(dom); } - destroy_signal(/** @type {import('./types.js').Effect} */ (block.e)); + destroy_effect(/** @type {import('./types.js').Effect} */ (block.e)); }); return component; @@ -2830,7 +2829,7 @@ export function prop(props, key, flags, initial) { return (inner_current_value.v = parent_value); }); - if (!immutable) current_value.e = safe_equal; + if (!immutable) current_value.equals = safe_equals; return function (/** @type {V} */ value, mutation = false) { var current = get(current_value); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index cdf5090d26..4db42363cd 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -1,16 +1,14 @@ import { DEV } from 'esm-env'; -import { run_all } from '../common.js'; import { array_prototype, get_descriptors, get_prototype_of, - is_array, is_frozen, object_freeze, object_prototype } from './utils.js'; import { unstate } from './proxy.js'; -import { pre_effect } from './reactivity/effects.js'; +import { destroy_effect, pre_effect } from './reactivity/effects.js'; import { EACH_BLOCK, IF_BLOCK, @@ -18,7 +16,6 @@ import { PRE_EFFECT, RENDER_EFFECT, DIRTY, - UNINITIALIZED, MAYBE_DIRTY, CLEAN, DERIVED, @@ -31,8 +28,7 @@ import { import { flush_tasks } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; import { mutate, set, source } from './reactivity/sources.js'; - -const IS_EFFECT = EFFECT | PRE_EFFECT | RENDER_EFFECT; +import { destroy_derived, update_derived } from './reactivity/deriveds.js'; const FLUSH_MICROTASK = 0; const FLUSH_SYNC = 1; @@ -55,10 +51,10 @@ let current_queued_pre_and_render_effects = []; let current_queued_effects = []; let flush_count = 0; -// Handle signal reactivity tree dependencies and consumer +// Handle signal reactivity tree dependencies and reactions /** @type {null | import('./types.js').Reaction} */ -export let current_consumer = null; +export let current_reaction = null; /** @type {null | import('./types.js').Effect} */ export let current_effect = null; @@ -96,8 +92,8 @@ export function set_ignore_mutation_validation(value) { } // If we are working with a get() chain that has no active container, -// to prevent memory leaks, we skip adding the consumer. -let current_skip_consumer = false; +// to prevent memory leaks, we skip adding the reaction. +export let current_skip_reaction = false; // Handle collecting all signals which are read during a specific time frame export let is_signals_recorded = false; let captured_signals = new Set(); @@ -116,15 +112,9 @@ export let current_block = null; /** @type {import('./types.js').ComponentContext | null} */ export let current_component_context = null; -export let updating_derived = false; - -/** - * @param {null | import('./types.js').ComponentContext} context - * @returns {boolean} - */ -export function is_runes(context) { - const component_context = context || current_component_context; - return component_context !== null && component_context.r; +/** @returns {boolean} */ +export function is_runes() { + return current_component_context !== null && current_component_context.r; } /** @@ -160,51 +150,53 @@ export function batch_inspect(target, prop, receiver) { } /** - * @param {import('./types.js').Signal} signal + * Determines whether a derived or effect is dirty. + * If it is MAYBE_DIRTY, will set the status to CLEAN + * @param {import('./types.js').Reaction} reaction * @returns {boolean} */ -function is_signal_dirty(signal) { - const flags = signal.f; - if ((flags & DIRTY) !== 0 || signal.v === UNINITIALIZED) { +function check_dirtiness(reaction) { + var flags = reaction.f; + + if ((flags & DIRTY) !== 0) { return true; } + if ((flags & MAYBE_DIRTY) !== 0) { - const dependencies = /** @type {import('./types.js').Reaction} **/ (signal).d; + var dependencies = reaction.deps; + if (dependencies !== null) { - const length = dependencies.length; - let i; - for (i = 0; i < length; i++) { - const dependency = dependencies[i]; - if ((dependency.f & MAYBE_DIRTY) !== 0 && !is_signal_dirty(dependency)) { - set_signal_status(dependency, CLEAN); - continue; - } - // The flags can be marked as dirty from the above is_signal_dirty call. - if ((dependency.f & DIRTY) !== 0) { - if ((dependency.f & DERIVED) !== 0) { - update_derived(/** @type {import('./types.js').Derived} **/ (dependency), true); - // Might have been mutated from above get. - if ((signal.f & DIRTY) !== 0) { - return true; - } - } else { + var length = dependencies.length; + + for (var i = 0; i < length; i++) { + var dependency = dependencies[i]; + + if (check_dirtiness(/** @type {import('#client').Derived} */ (dependency))) { + update_derived(/** @type {import('#client').Derived} **/ (dependency), true); + + // `signal` might now be dirty, as a result of calling `update_derived` + if ((reaction.f & DIRTY) !== 0) { return true; } } - // If we're workig with an unowned derived signal, then we need to check - // if our dependency write version is higher. If is is then we can assume + + // If we're working with an unowned derived signal, then we need to check + // if our dependency write version is higher. If it is then we can assume // that state has changed to a newer version and thus this unowned signal // is also dirty. - const is_unowned = (flags & UNOWNED) !== 0; - const write_version = signal.w; - const dep_write_version = dependency.w; - if (is_unowned && dep_write_version > write_version) { - signal.w = dep_write_version; + var is_unowned = (flags & UNOWNED) !== 0; + var version = dependency.version; + + if (is_unowned && version > /** @type {import('#client').Derived} */ (reaction).version) { + /** @type {import('#client').Derived} */ (reaction).version = version; return true; } } } + + set_signal_status(reaction, CLEAN); } + return false; } @@ -213,41 +205,40 @@ function is_signal_dirty(signal) { * @param {import('./types.js').Reaction} signal * @returns {V} */ -function execute_signal_fn(signal) { - const init = signal.i; +export function execute_reaction_fn(signal) { + const fn = signal.fn; const flags = signal.f; + const is_render_effect = (flags & RENDER_EFFECT) !== 0; + const previous_dependencies = current_dependencies; const previous_dependencies_index = current_dependencies_index; const previous_untracked_writes = current_untracked_writes; - const previous_consumer = current_consumer; - const previous_block = current_block; - const previous_component_context = current_component_context; - const previous_skip_consumer = current_skip_consumer; - const is_render_effect = (flags & RENDER_EFFECT) !== 0; + const previous_reaction = current_reaction; + const previous_skip_reaction = current_skip_reaction; const previous_untracking = current_untracking; + current_dependencies = /** @type {null | import('./types.js').Value[]} */ (null); current_dependencies_index = 0; current_untracked_writes = null; - current_consumer = signal; - current_block = signal.b; - current_component_context = signal.x; - current_skip_consumer = !is_flushing_effect && (flags & UNOWNED) !== 0; + current_reaction = signal; + current_skip_reaction = !is_flushing_effect && (flags & UNOWNED) !== 0; current_untracking = false; try { let res; if (is_render_effect) { - res = - /** @type {(block: import('./types.js').Block, signal: import('./types.js').Signal) => V} */ ( - init - )( - /** @type {import('./types.js').Block} */ (signal.b), - /** @type {import('./types.js').Signal} */ (signal) - ); + res = /** @type {(block: import('#client').Block, signal: import('#client').Signal) => V} */ ( + fn + )( + /** @type {import('#client').Block} */ ( + /** @type {import('#client').Effect} */ (signal).block + ), + /** @type {import('#client').Signal} */ (signal) + ); } else { - res = /** @type {() => V} */ (init)(); + res = /** @type {() => V} */ (fn)(); } - let dependencies = /** @type {import('./types.js').Value[]} **/ (signal.d); + let dependencies = /** @type {import('./types.js').Value[]} **/ (signal.deps); if (current_dependencies !== null) { let i; if (dependencies !== null) { @@ -271,7 +262,7 @@ function execute_signal_fn(signal) { ? !full_current_dependencies_set.has(dependency) : !full_current_dependencies.includes(dependency) ) { - remove_consumer(signal, dependency); + remove_reaction(signal, dependency); } } } @@ -282,29 +273,29 @@ function execute_signal_fn(signal) { dependencies[current_dependencies_index + i] = current_dependencies[i]; } } else { - signal.d = /** @type {import('./types.js').Value[]} **/ ( + signal.deps = /** @type {import('./types.js').Value[]} **/ ( dependencies = current_dependencies ); } - if (!current_skip_consumer) { + if (!current_skip_reaction) { for (i = current_dependencies_index; i < dependencies.length; i++) { const dependency = dependencies[i]; - const consumers = dependency.c; + const reactions = dependency.reactions; - if (consumers === null) { - dependency.c = [signal]; - } else if (consumers[consumers.length - 1] !== signal) { + if (reactions === null) { + dependency.reactions = [signal]; + } else if (reactions[reactions.length - 1] !== signal) { // TODO: should this be: // - // } else if (!consumers.includes(signal)) { + // } else if (!reactions.includes(signal)) { // - consumers.push(signal); + reactions.push(signal); } } } } else if (dependencies !== null && current_dependencies_index < dependencies.length) { - remove_consumers(signal, current_dependencies_index); + remove_reactions(signal, current_dependencies_index); dependencies.length = current_dependencies_index; } return res; @@ -312,10 +303,8 @@ function execute_signal_fn(signal) { current_dependencies = previous_dependencies; current_dependencies_index = previous_dependencies_index; current_untracked_writes = previous_untracked_writes; - current_consumer = previous_consumer; - current_block = previous_block; - current_component_context = previous_component_context; - current_skip_consumer = previous_skip_consumer; + current_reaction = previous_reaction; + current_skip_reaction = previous_skip_reaction; current_untracking = previous_untracking; } } @@ -326,26 +315,26 @@ function execute_signal_fn(signal) { * @param {import('./types.js').Value} dependency * @returns {void} */ -function remove_consumer(signal, dependency) { - const consumers = dependency.c; - let consumers_length = 0; - if (consumers !== null) { - consumers_length = consumers.length - 1; - const index = consumers.indexOf(signal); +function remove_reaction(signal, dependency) { + const reactions = dependency.reactions; + let reactions_length = 0; + if (reactions !== null) { + reactions_length = reactions.length - 1; + const index = reactions.indexOf(signal); if (index !== -1) { - if (consumers_length === 0) { - dependency.c = null; + if (reactions_length === 0) { + dependency.reactions = null; } else { // Swap with last element and then remove. - consumers[index] = consumers[consumers_length]; - consumers.pop(); + reactions[index] = reactions[reactions_length]; + reactions.pop(); } } } - if (consumers_length === 0 && (dependency.f & UNOWNED) !== 0) { + if (reactions_length === 0 && (dependency.f & UNOWNED) !== 0) { // If the signal is unowned then we need to make sure to change it to dirty. set_signal_status(dependency, DIRTY); - remove_consumers(/** @type {import('./types.js').Reaction} **/ (dependency), 0); + remove_reactions(/** @type {import('./types.js').Derived} **/ (dependency), 0); } } @@ -354,16 +343,16 @@ function remove_consumer(signal, dependency) { * @param {number} start_index * @returns {void} */ -function remove_consumers(signal, start_index) { - const dependencies = signal.d; +export function remove_reactions(signal, start_index) { + const dependencies = signal.deps; if (dependencies !== null) { const active_dependencies = start_index === 0 ? null : dependencies.slice(0, start_index); let i; for (i = start_index; i < dependencies.length; i++) { const dependency = dependencies[i]; - // Avoid removing a consumer if we know that it is active (start_index will not be 0) + // Avoid removing a reaction if we know that it is active (start_index will not be 0) if (active_dependencies === null || !active_dependencies.includes(dependency)) { - remove_consumer(signal, dependency); + remove_reaction(signal, dependency); } } } @@ -373,28 +362,19 @@ function remove_consumers(signal, start_index) { * @param {import('./types.js').Reaction} signal * @returns {void} */ -function destroy_references(signal) { - const references = signal.r; - signal.r = null; - if (references !== null) { - let i; - for (i = 0; i < references.length; i++) { - destroy_signal(references[i]); +export function destroy_children(signal) { + if (signal.effects) { + for (var i = 0; i < signal.effects.length; i += 1) { + destroy_effect(signal.effects[i]); } + signal.effects = null; } -} - -/** - * @param {import('./types.js').Block} block - * @param {unknown} error - * @returns {void} - */ -function report_error(block, error) { - /** @type {import('./types.js').Block | null} */ - let current_block = block; - if (current_block !== null) { - throw error; + if (signal.deriveds) { + for (i = 0; i < signal.deriveds.length; i += 1) { + destroy_derived(signal.deriveds[i]); + } + signal.deriveds = null; } } @@ -406,30 +386,28 @@ export function execute_effect(signal) { if ((signal.f & DESTROYED) !== 0) { return; } - const teardown = signal.v; + const previous_effect = current_effect; + const previous_component_context = current_component_context; + const previous_block = current_block; + + const component_context = signal.ctx; + current_effect = signal; + current_component_context = component_context; + current_block = signal.block; try { - destroy_references(signal); - if (teardown !== null) { - teardown(); - } - const possible_teardown = execute_signal_fn(signal); - if (typeof possible_teardown === 'function') { - signal.v = possible_teardown; - } - } catch (error) { - const block = signal.b; - if (block !== null) { - report_error(block, error); - } else { - throw error; - } + destroy_children(signal); + signal.teardown?.(); + const teardown = execute_reaction_fn(signal); + signal.teardown = typeof teardown === 'function' ? teardown : null; } finally { current_effect = previous_effect; + current_component_context = previous_component_context; + current_block = previous_block; } - const component_context = signal.x; + if ((signal.f & PRE_EFFECT) !== 0 && current_queued_pre_and_render_effects.length > 0) { flush_local_pre_effects(component_context); } @@ -454,31 +432,29 @@ function infinite_loop_guard() { * @returns {void} */ function flush_queued_effects(effects) { - const length = effects.length; - if (length > 0) { - infinite_loop_guard(); - const previously_flushing_effect = is_flushing_effect; - is_flushing_effect = true; - try { - let i; - for (i = 0; i < length; i++) { - const signal = effects[i]; - const flags = signal.f; - if ((flags & (DESTROYED | INERT)) === 0) { - if (is_signal_dirty(signal)) { - set_signal_status(signal, CLEAN); - execute_effect(signal); - } else if ((flags & MAYBE_DIRTY) !== 0) { - set_signal_status(signal, CLEAN); - } + var length = effects.length; + if (length === 0) return; + + infinite_loop_guard(); + var previously_flushing_effect = is_flushing_effect; + is_flushing_effect = true; + + try { + for (var i = 0; i < length; i++) { + var signal = effects[i]; + + if ((signal.f & (DESTROYED | INERT)) === 0) { + if (check_dirtiness(signal)) { + set_signal_status(signal, CLEAN); + execute_effect(signal); } } - } finally { - is_flushing_effect = previously_flushing_effect; } - - effects.length = 0; + } finally { + is_flushing_effect = previously_flushing_effect; } + + effects.length = 0; } function process_microtask() { @@ -540,7 +516,7 @@ export function schedule_effect(signal, sync) { if (!should_append) { const target_level = signal.l; - const target_block = signal.b; + const target_block = signal.block; const is_pre_effect = (flags & PRE_EFFECT) !== 0; let target_signal; let is_target_pre_effect; @@ -552,7 +528,10 @@ export function schedule_effect(signal, sync) { should_append = true; } else { is_target_pre_effect = (target_signal.f & PRE_EFFECT) !== 0; - if (target_signal.b !== target_block || (is_target_pre_effect && !is_pre_effect)) { + if ( + target_signal.block !== target_block || + (is_target_pre_effect && !is_pre_effect) + ) { i++; } current_queued_pre_and_render_effects.splice(i, 0, signal); @@ -580,7 +559,7 @@ export function flush_local_render_effects() { const effects = []; for (let i = 0; i < current_queued_pre_and_render_effects.length; i++) { const effect = current_queued_pre_and_render_effects[i]; - if ((effect.f & RENDER_EFFECT) !== 0 && effect.x === current_component_context) { + if ((effect.f & RENDER_EFFECT) !== 0 && effect.ctx === current_component_context) { effects.push(effect); current_queued_pre_and_render_effects.splice(i, 1); i--; @@ -597,7 +576,7 @@ export function flush_local_pre_effects(context) { const effects = []; for (let i = 0; i < current_queued_pre_and_render_effects.length; i++) { const effect = current_queued_pre_and_render_effects[i]; - if ((effect.f & PRE_EFFECT) !== 0 && effect.x === context) { + if ((effect.f & PRE_EFFECT) !== 0 && effect.ctx === context) { effects.push(effect); current_queued_pre_and_render_effects.splice(i, 1); i--; @@ -670,34 +649,6 @@ export async function tick() { flushSync(); } -/** - * @param {import('./types.js').Derived} signal - * @param {boolean} force_schedule - * @returns {void} - */ -function update_derived(signal, force_schedule) { - const previous_updating_derived = updating_derived; - updating_derived = true; - destroy_references(signal); - const value = execute_signal_fn(signal); - updating_derived = previous_updating_derived; - const status = - (current_skip_consumer || (signal.f & UNOWNED) !== 0) && signal.d !== null - ? MAYBE_DIRTY - : CLEAN; - set_signal_status(signal, status); - const equals = /** @type {import('./types.js').EqualsFunctions} */ (signal.e); - if (!equals(value, signal.v)) { - signal.v = value; - mark_signal_consumers(signal, DIRTY, force_schedule); - - // @ts-expect-error - if (DEV && signal.inspect && force_schedule) { - for (const fn of /** @type {import('./types.js').ValueDebug} */ (signal).inspect) fn(); - } - } -} - /** * @template V * @param {import('./types.js').Value} signal @@ -720,10 +671,10 @@ export function get(signal) { captured_signals.add(signal); } - // Register the dependency on the current consumer signal. - if (current_consumer !== null && (current_consumer.f & MANAGED) === 0 && !current_untracking) { - const unowned = (current_consumer.f & UNOWNED) !== 0; - const dependencies = current_consumer.d; + // Register the dependency on the current reaction signal. + if (current_reaction !== null && (current_reaction.f & MANAGED) === 0 && !current_untracking) { + const unowned = (current_reaction.f & UNOWNED) !== 0; + const dependencies = current_reaction.deps; if ( current_dependencies === null && dependencies !== null && @@ -754,7 +705,10 @@ export function get(signal) { } } - if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) { + if ( + (flags & DERIVED) !== 0 && + check_dirtiness(/** @type {import('#client').Derived} */ (signal)) + ) { if (DEV) { // we want to avoid tracking indirect dependencies const previous_inspect_fn = inspect_fn; @@ -797,28 +751,25 @@ export function invalidate_inner_signals(fn) { } /** - * @param {import('./types.js').Reaction} signal + * @param {import('#client').Effect} signal * @param {boolean} inert - * @param {Set} [visited_blocks] + * @param {Set} [visited_blocks] * @returns {void} */ function mark_subtree_children_inert(signal, inert, visited_blocks) { - const references = signal.r; - if (references !== null) { - let i; - for (i = 0; i < references.length; i++) { - const reference = references[i]; - if ((reference.f & IS_EFFECT) !== 0) { - mark_subtree_inert(reference, inert, visited_blocks); - } + const effects = signal.effects; + if (effects !== null) { + for (var i = 0; i < effects.length; i++) { + const effect = effects[i]; + mark_subtree_inert(effect, inert, visited_blocks); } } } /** - * @param {import('./types.js').Reaction} signal + * @param {import('#client').Effect} signal * @param {boolean} inert - * @param {Set} [visited_blocks] + * @param {Set} [visited_blocks] * @returns {void} */ export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) { @@ -826,11 +777,11 @@ export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) { const is_already_inert = (flags & INERT) !== 0; if (is_already_inert !== inert) { signal.f ^= INERT; - if (!inert && (flags & IS_EFFECT) !== 0 && (flags & CLEAN) === 0) { - schedule_effect(/** @type {import('./types.js').Effect} */ (signal), false); + if (!inert && (flags & CLEAN) === 0) { + schedule_effect(signal, false); } // Nested if block effects - const block = signal.b; + const block = signal.block; if (block !== null && !visited_blocks.has(block)) { visited_blocks.add(block); const type = block.t; @@ -861,65 +812,49 @@ export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) { } /** - * @param {import('./types.js').Signal} signal + * @param {import('#client').Value} signal * @param {number} to_status * @param {boolean} force_schedule * @returns {void} */ -export function mark_signal_consumers(signal, to_status, force_schedule) { - const runes = is_runes(null); - const consumers = signal.c; - if (consumers !== null) { - const length = consumers.length; - let i; - for (i = 0; i < length; i++) { - const consumer = consumers[i]; - const flags = consumer.f; - const unowned = (flags & UNOWNED) !== 0; - // We skip any effects that are already dirty (but not unowned). Additionally, we also - // skip if the consumer is the same as the current effect (except if we're not in runes or we - // are in force schedule mode). - if ((!force_schedule || !runes) && consumer === current_effect) { - continue; - } - set_signal_status(consumer, to_status); - // If the signal is not clean, then skip over it – with the exception of unowned signals that - // are already maybe dirty. Unowned signals might be dirty because they are not captured as part of an - // effect. - const maybe_dirty = (flags & MAYBE_DIRTY) !== 0; - if ((flags & CLEAN) !== 0 || (maybe_dirty && unowned)) { - if ((consumer.f & IS_EFFECT) !== 0) { - schedule_effect(/** @type {import('./types.js').Effect} */ (consumer), false); - } else { - mark_signal_consumers(consumer, MAYBE_DIRTY, force_schedule); - } - } +export function mark_reactions(signal, to_status, force_schedule) { + var reactions = signal.reactions; + if (reactions === null) return; + + var runes = is_runes(); + var length = reactions.length; + + for (var i = 0; i < length; i++) { + var reaction = reactions[i]; + + // We skip any effects that are already dirty (but not unowned). Additionally, we also + // skip if the reaction is the same as the current effect (except if we're not in runes or we + // are in force schedule mode). + if ((!force_schedule || !runes) && reaction === current_effect) { + continue; } - } -} -/** - * @param {import('./types.js').Reaction} signal - * @returns {void} - */ -export function destroy_signal(signal) { - const teardown = /** @type {null | (() => void)} */ (signal.v); - const destroy = signal.y; - const flags = signal.f; - destroy_references(signal); - remove_consumers(signal, 0); - signal.i = signal.r = signal.y = signal.x = signal.b = signal.d = signal.c = null; - set_signal_status(signal, DESTROYED); - if (destroy !== null) { - if (is_array(destroy)) { - run_all(destroy); - } else { - destroy(); + var flags = reaction.f; + set_signal_status(reaction, to_status); + + // If the signal is not clean, then skip over it – with the exception of unowned signals that + // are already maybe dirty. Unowned signals might be dirty because they are not captured as part of an + // effect. + var maybe_dirty = (flags & MAYBE_DIRTY) !== 0; + var unowned = (flags & UNOWNED) !== 0; + + if ((flags & CLEAN) !== 0 || (maybe_dirty && unowned)) { + if ((reaction.f & DERIVED) !== 0) { + mark_reactions( + /** @type {import('#client').Derived} */ (reaction), + MAYBE_DIRTY, + force_schedule + ); + } else { + schedule_effect(/** @type {import('#client').Effect} */ (reaction), false); + } } } - if (teardown !== null && (flags & IS_EFFECT) !== 0) { - teardown(); - } } /** @@ -940,22 +875,6 @@ export function untrack(fn) { } } -/** - * @param {import('./types.js').Reaction} signal - * @param {() => void} destroy_fn - * @returns {void} - */ -export function push_destroy_fn(signal, destroy_fn) { - let destroy = signal.y; - if (destroy === null) { - signal.y = destroy_fn; - } else if (is_array(destroy)) { - destroy.push(destroy_fn); - } else { - signal.y = [destroy, destroy_fn]; - } -} - const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN); /** @@ -1350,8 +1269,8 @@ export function inspect(get_value, inspect = console.log) { /** * @template V - * @param {V} value - * @returns {import('./types.js').UnwrappedSignal} + * @param {V | import('#client').Value} value + * @returns {V} */ export function unwrap(value) { if (is_signal(value)) { diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index 3261c42833..416f38f592 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -12,11 +12,15 @@ import { import { destroy_each_item_block, get_first_element } from './dom/blocks/each.js'; import { schedule_raf_task } from './dom/task.js'; import { append_child, empty } from './operations.js'; -import { effect, managed_effect, managed_pre_effect } from './reactivity/effects.js'; +import { + destroy_effect, + effect, + managed_effect, + managed_pre_effect +} from './reactivity/effects.js'; import { current_block, current_effect, - destroy_signal, execute_effect, mark_subtree_inert, untrack @@ -589,7 +593,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo } const effect = managed_pre_effect(() => { - destroy_signal(effect); + destroy_effect(effect); dom.inert = false; if (show_intro && !already_mounted) { @@ -613,7 +617,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo } transition_block = parent; } - }, false); + }); }); if (direction === 'key') { @@ -666,12 +670,12 @@ export function trigger_transitions(transitions, target_direction, from) { if (outros.length > 0) { // Defer the outros to a microtask const e = managed_pre_effect(() => { - destroy_signal(e); + destroy_effect(e); const e2 = managed_effect(() => { - destroy_signal(e2); + destroy_effect(e2); run_all(outros); }); - }, false); + }); } } diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index bb0b26209e..e4f776715a 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -11,7 +11,7 @@ import { SNIPPET_BLOCK, STATE_SYMBOL } from './constants.js'; -import type { Reaction, Effect, Signal, Source, Value } from './reactivity/types.js'; +import type { Effect, Source, Value } from './reactivity/types.js'; type EventCallback = (event: Event) => boolean; export type EventCallbackMap = Record; @@ -57,7 +57,7 @@ export type ComponentContext = { }; }; -export type EqualsFunctions = (a: T, v: T) => boolean; +export type Equals = (this: Value, value: unknown) => boolean; export type BlockType = | typeof ROOT_BLOCK @@ -99,7 +99,7 @@ export type RootBlock = { /** dom */ d: null | TemplateNode | Array; /** effect */ - e: null | Reaction; + e: null | Effect; /** intro */ i: boolean; /** parent */ @@ -150,7 +150,7 @@ export type HeadBlock = { /** dom */ d: null | TemplateNode | Array; /** effect */ - e: null | Reaction; + e: null | Effect; /** parent */ p: Block; /** transition */ @@ -163,7 +163,7 @@ export type DynamicElementBlock = { /** dom */ d: null | TemplateNode | Array; /** effect */ - e: null | Reaction; + e: null | Effect; /** parent */ p: Block; /** transition */ @@ -176,7 +176,7 @@ export type DynamicComponentBlock = { /** dom */ d: null | TemplateNode | Array; /** effect */ - e: null | Reaction; + e: null | Effect; /** parent */ p: Block; /** transition */ @@ -189,7 +189,7 @@ export type AwaitBlock = { /** dom */ d: null | TemplateNode | Array; /** effect */ - e: null | Reaction; + e: null | Effect; /** parent */ p: Block; /** pending */ @@ -210,7 +210,7 @@ export type EachBlock = { /** items */ v: EachItemBlock[]; /** effewct */ - e: null | Reaction; + e: null | Effect; /** parent */ p: Block; /** transition */ @@ -250,7 +250,7 @@ export type SnippetBlock = { /** parent */ p: Block; /** effect */ - e: null | Reaction; + e: null | Effect; /** transition */ r: null; /** type */ diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 8791a434d4..248046073c 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1,6 +1,11 @@ import { describe, assert, it } from 'vitest'; import * as $ from '../../src/internal/client/runtime'; -import { effect, render_effect, user_effect } from '../../src/internal/client/reactivity/effects'; +import { + destroy_effect, + effect, + render_effect, + user_effect +} from '../../src/internal/client/reactivity/effects'; import { source, set } from '../../src/internal/client/reactivity/sources'; import type { Derived } from '../../src/internal/client/types'; import { proxy } from '../../src/internal/client/proxy'; @@ -27,7 +32,7 @@ function run_test(runes: boolean, fn: (runes: boolean) => () => void) { ); $.pop(); execute(); - $.destroy_signal(signal); + destroy_effect(signal); }; } @@ -193,13 +198,13 @@ describe('signals', () => { return () => { $.flushSync(() => set(count, 1)); // Ensure we're not leaking consumers - assert.deepEqual(count.c?.length, 1); + assert.deepEqual(count.reactions?.length, 1); $.flushSync(() => set(count, 2)); // Ensure we're not leaking consumers - assert.deepEqual(count.c?.length, 1); + assert.deepEqual(count.reactions?.length, 1); $.flushSync(() => set(count, 3)); // Ensure we're not leaking consumers - assert.deepEqual(count.c?.length, 1); + assert.deepEqual(count.reactions?.length, 1); assert.deepEqual(log, [0, 1, 2, 3]); }; }); @@ -230,7 +235,7 @@ describe('signals', () => { // Ensure we're not leaking dependencies assert.deepEqual( - nested.slice(0, -2).map((s) => s.d), + nested.slice(0, -2).map((s) => s.deps), [null, null] ); }; @@ -259,11 +264,11 @@ describe('signals', () => { $.flushSync(() => set(count, 4)); $.flushSync(() => set(count, 0)); // Ensure we're not leaking consumers - assert.deepEqual(count.c?.length, 1); + assert.deepEqual(count.reactions?.length, 1); assert.deepEqual(log, [0, 2, 'limit', 0]); - $.destroy_signal(effect); + destroy_effect(effect); // Ensure we're not leaking consumers - assert.deepEqual(count.c, null); + assert.deepEqual(count.reactions, null); }; }); @@ -351,4 +356,26 @@ describe('signals', () => { assert.equal(errored, true); }; }); + + test('effect teardown is removed on re-run', () => { + const count = source(0); + let first = true; + let teardown = 0; + + user_effect(() => { + $.get(count); + if (first) { + first = false; + return () => { + teardown += 1; + }; + } + }); + + return () => { + $.flushSync(() => set(count, 1)); + $.flushSync(() => set(count, 2)); + assert.equal(teardown, 1); + }; + }); });