diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js index b524391380..95347d8f12 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-runes.js @@ -389,7 +389,7 @@ export const javascript_visitors_runes = { const args = /** @type {import('estree').Expression[]} */ ( node.arguments.map((arg) => context.visit(arg)) ); - return b.call('$.user_root_effect', ...args); + return b.call('$.effect_root', ...args); } if (rune === '$inspect' || rune === '$inspect().with') { diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index 17917d848a..12983dab32 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -2,7 +2,8 @@ export const DERIVED = 1 << 1; export const EFFECT = 1 << 2; export const PRE_EFFECT = 1 << 3; export const RENDER_EFFECT = 1 << 4; -export const MANAGED = 1 << 6; +export const BLOCK_EFFECT = 1 << 5; +export const BRANCH_EFFECT = 1 << 6; export const UNOWNED = 1 << 7; export const CLEAN = 1 << 8; export const DIRTY = 1 << 9; @@ -11,6 +12,7 @@ export const INERT = 1 << 11; export const DESTROYED = 1 << 12; export const IS_ELSEIF = 1 << 13; export const EFFECT_RAN = 1 << 14; +export const ROOT_EFFECT = 1 << 15; export const UNINITIALIZED = Symbol(); export const STATE_SYMBOL = Symbol('$state'); diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 6afb629e2f..fb7fadc647 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -1,5 +1,4 @@ import { is_promise } from '../../../common.js'; -import { remove } from '../reconciler.js'; import { current_component_context, flushSync, @@ -7,7 +6,7 @@ import { set_current_effect, set_current_reaction } from '../../runtime.js'; -import { destroy_effect, pause_effect, render_effect } from '../../reactivity/effects.js'; +import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js'; import { INERT } from '../../constants.js'; /** @@ -39,10 +38,10 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { * @param {any} value */ function create_effect(fn, value) { - set_current_effect(branch); - set_current_reaction(branch); // TODO do we need both? + set_current_effect(effect); + set_current_reaction(effect); // TODO do we need both? set_current_component_context(component_context); - var effect = render_effect(() => fn(anchor, value), true); + var e = branch(() => fn(anchor, value)); set_current_component_context(null); set_current_reaction(null); set_current_effect(null); @@ -51,10 +50,10 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { // resolves which is unexpected behaviour (and somewhat irksome to test) flushSync(); - return effect; + return e; } - const branch = render_effect(() => { + const effect = block(() => { if (input === (input = get_input())) return; if (is_promise(input)) { @@ -62,11 +61,10 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { if (pending_fn) { if (pending_effect && (pending_effect.f & INERT) === 0) { - if (pending_effect.dom) remove(pending_effect.dom); destroy_effect(pending_effect); } - pending_effect = render_effect(() => pending_fn(anchor), true); + pending_effect = branch(() => pending_fn(anchor)); } if (then_effect) pause_effect(then_effect); @@ -96,19 +94,11 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { if (then_fn) { if (then_effect) { - if (then_effect.dom) remove(then_effect.dom); destroy_effect(then_effect); } - then_effect = render_effect(() => then_fn(anchor, input), true); + then_effect = branch(() => then_fn(anchor, input)); } } }); - - branch.ondestroy = () => { - // TODO this sucks, tidy it up - if (pending_effect?.dom) remove(pending_effect.dom); - if (then_effect?.dom) remove(then_effect.dom); - if (catch_effect?.dom) remove(catch_effect.dom); - }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/css-props.js b/packages/svelte/src/internal/client/dom/blocks/css-props.js index d9e3e8b088..709d760700 100644 --- a/packages/svelte/src/internal/client/dom/blocks/css-props.js +++ b/packages/svelte/src/internal/client/dom/blocks/css-props.js @@ -39,26 +39,28 @@ export function css_props(anchor, is_html, props, component) { component(component_anchor); - /** @type {Record} */ - let current_props = {}; + render_effect(() => { + /** @type {Record} */ + let current_props = {}; - const effect = render_effect(() => { - const next_props = props(); + render_effect(() => { + const next_props = props(); - for (const key in current_props) { - if (!(key in next_props)) { - element.style.removeProperty(key); + for (const key in current_props) { + if (!(key in next_props)) { + element.style.removeProperty(key); + } } - } - for (const key in next_props) { - element.style.setProperty(key, next_props[key]); - } + for (const key in next_props) { + element.style.setProperty(key, next_props[key]); + } - current_props = next_props; - }); + current_props = next_props; + }); - effect.ondestroy = () => { - remove(element); - }; + return () => { + remove(element); + }; + }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index c23dabffae..e9db779e20 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -11,11 +11,11 @@ import { empty } from '../operations.js'; import { insert, remove } from '../reconciler.js'; import { untrack } from '../../runtime.js'; import { - destroy_effect, + block, + branch, effect, pause_effect, pause_effects, - render_effect, resume_effect } from '../../reactivity/effects.js'; import { source, mutable_source, set } from '../../reactivity/sources.js'; @@ -43,7 +43,7 @@ export function set_current_each_item(item) { * @param {number} flags * @param {() => V[]} get_collection * @param {null | ((item: V) => string)} get_key - * @param {(anchor: Node, item: V, index: import('#client').MaybeSource) => void} render_fn + * @param {(anchor: Node, item: import('#client').MaybeSource, 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} @@ -67,7 +67,7 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re /** @type {import('#client').Effect | null} */ var fallback = null; - var effect = render_effect(() => { + block(() => { var collection = get_collection(); var array = is_array(collection) @@ -152,15 +152,7 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re if (fallback) { resume_effect(fallback); } else { - fallback = render_effect(() => { - var dom = fallback_fn(anchor); - - return () => { - if (dom !== undefined) { - remove(dom); - } - }; - }, true); + fallback = branch(() => fallback_fn(anchor)); } } else if (fallback !== null) { pause_effect(fallback, () => { @@ -174,17 +166,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re set_hydrating(true); } }); - - effect.ondestroy = () => { - for (var item of state.items) { - if (item.e.dom !== null) { - remove(item.e.dom); - destroy_effect(item.e); - } - } - - if (fallback) destroy_effect(fallback); - }; } /** @@ -193,7 +174,7 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re * @param {number} flags * @param {() => V[]} get_collection * @param {null | ((item: V) => string)} get_key - * @param {(anchor: Node, item: V, index: import('#client').MaybeSource) => void} render_fn + * @param {(anchor: Node, item: import('#client').MaybeSource, index: import('#client').MaybeSource) => void} render_fn * @param {null | ((anchor: Node) => void)} [fallback_fn] * @returns {void} */ @@ -206,7 +187,7 @@ export function each_keyed(anchor, flags, get_collection, get_key, render_fn, fa * @param {Element | Comment} anchor * @param {number} flags * @param {() => V[]} get_collection - * @param {(anchor: Node, item: V, index: import('#client').MaybeSource) => void} render_fn + * @param {(anchor: Node, item: import('#client').MaybeSource, index: import('#client').MaybeSource) => void} render_fn * @param {null | ((anchor: Node) => void)} [fallback_fn] * @returns {void} */ @@ -219,7 +200,7 @@ export function each_indexed(anchor, flags, get_collection, render_fn, fallback_ * @param {Array} array * @param {import('#client').EachState} state * @param {Element | Comment | Text} anchor - * @param {(anchor: Node, item: V, index: number | import('#client').Source) => void} render_fn + * @param {(anchor: Node, item: import('#client').MaybeSource, index: number | import('#client').Source) => void} render_fn * @param {number} flags * @returns {void} */ @@ -275,7 +256,7 @@ function reconcile_indexed_array(array, state, anchor, render_fn, flags) { * @param {Array} array * @param {import('#client').EachState} state * @param {Element | Comment | Text} anchor - * @param {(anchor: Node, item: V, index: number | import('#client').Source) => void} render_fn + * @param {(anchor: Node, item: import('#client').MaybeSource, index: number | import('#client').Source) => void} render_fn * @param {number} flags * @param {any[]} keys * @returns {void} @@ -342,7 +323,6 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) { var i; var index; var last_item; - var last_sibling; // store the indexes of each item in the new world for (i = start; i < b; i += 1) { @@ -548,45 +528,32 @@ function update_item(item, value, index, type) { * @param {V} value * @param {unknown} key * @param {number} index - * @param {(anchor: Node, item: V, index: number | import('#client').Value) => void} render_fn + * @param {(anchor: Node, item: V | import('#client').Source, index: number | import('#client').Value) => void} render_fn * @param {number} flags * @returns {import('#client').EachItem} */ function create_item(anchor, value, key, index, render_fn, flags) { - var each_item_not_reactive = (flags & EACH_ITEM_REACTIVE) === 0; - - /** @type {import('#client').EachItem} */ - var item = { - a: null, - // dom - // @ts-expect-error - e: null, - // index - i: (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index), - // key - k: key, - // item - v: each_item_not_reactive - ? value - : (flags & EACH_IS_STRICT_EQUALS) !== 0 - ? source(value) - : mutable_source(value) - }; - var previous_each_item = current_each_item; try { - current_each_item = item; - - item.e = render_effect(() => { - var dom = render_fn(anchor, item.v, item.i); + var reactive = (flags & EACH_ITEM_REACTIVE) !== 0; + var mutable = (flags & EACH_IS_STRICT_EQUALS) === 0; + + var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value; + var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index); + + /** @type {import('#client').EachItem} */ + var item = { + i, + v, + k: key, + a: null, + // @ts-expect-error + e: null + }; - return () => { - if (dom !== undefined) { - remove(dom); - } - }; - }, true); + current_each_item = item; + item.e = branch(() => render_fn(anchor, v, i)); return item; } finally { diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 0a57d46af2..31fcc3d2e2 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -1,29 +1,22 @@ +import { derived } from '../../reactivity/deriveds.js'; import { render_effect } from '../../reactivity/effects.js'; +import { get } from '../../runtime.js'; import { reconcile_html, remove } from '../reconciler.js'; /** - * @param {Element | Text | Comment} dom + * @param {Element | Text | Comment} anchor * @param {() => string} get_value * @param {boolean} svg * @returns {void} */ -export function html(dom, get_value, svg) { - /** @type {import('#client').Dom} */ - let html_dom; +export function html(anchor, get_value, svg) { + let value = derived(get_value); - /** @type {string} */ - let value; + render_effect(() => { + var dom = reconcile_html(anchor, get(value), svg); - const effect = render_effect(() => { - if (value !== (value = get_value())) { - if (html_dom) remove(html_dom); - html_dom = reconcile_html(dom, value, svg); + if (dom) { + return () => remove(dom); } }); - - effect.ondestroy = () => { - if (html_dom) { - remove(html_dom); - } - }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index d8e252c3f3..27accd8d61 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -1,12 +1,7 @@ import { IS_ELSEIF } from '../../constants.js'; import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js'; import { remove } from '../reconciler.js'; -import { - destroy_effect, - pause_effect, - render_effect, - resume_effect -} from '../../reactivity/effects.js'; +import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js'; /** * @param {Comment} anchor @@ -32,7 +27,7 @@ export function if_block( /** @type {boolean | null} */ let condition = null; - const if_effect = render_effect(() => { + const effect = block(() => { if (condition === (condition = !!get_condition())) return; /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ @@ -61,7 +56,7 @@ export function if_block( if (consequent_effect) { resume_effect(consequent_effect); } else { - consequent_effect = render_effect(() => consequent_fn(anchor), true); + consequent_effect = branch(() => consequent_fn(anchor)); } if (alternate_effect) { @@ -73,7 +68,7 @@ export function if_block( if (alternate_effect) { resume_effect(alternate_effect); } else if (alternate_fn) { - alternate_effect = render_effect(() => alternate_fn(anchor), true); + alternate_effect = branch(() => alternate_fn(anchor)); } if (consequent_effect) { @@ -90,17 +85,6 @@ export function if_block( }); if (elseif) { - if_effect.f |= IS_ELSEIF; + effect.f |= IS_ELSEIF; } - - if_effect.ondestroy = () => { - // TODO why is this not automatic? this should be children of `if_effect` - if (consequent_effect) { - destroy_effect(consequent_effect); - } - - if (alternate_effect) { - destroy_effect(alternate_effect); - } - }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index 66cdb3ed68..07d138b15f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -1,6 +1,5 @@ import { UNINITIALIZED } from '../../constants.js'; -import { remove } from '../reconciler.js'; -import { pause_effect, render_effect } from '../../reactivity/effects.js'; +import { block, branch, pause_effect } from '../../reactivity/effects.js'; import { safe_not_equal } from '../../reactivity/equality.js'; /** @@ -17,39 +16,13 @@ export function key_block(anchor, get_key, render_fn) { /** @type {import('#client').Effect} */ let effect; - /** - * Every time `key` changes, we create a new effect. Old effects are - * removed from this set when they have fully transitioned out - * @type {Set} - */ - let effects = new Set(); - - const key_effect = render_effect(() => { + block(() => { if (safe_not_equal(key, (key = get_key()))) { if (effect) { - var e = effect; - pause_effect(e, () => { - effects.delete(e); - }); + pause_effect(effect); } - effect = render_effect(() => { - const dom = render_fn(anchor); - - return () => { - if (dom !== undefined) { - remove(dom); - } - }; - }, true); - - effects.add(effect); + effect = branch(() => render_fn(anchor)); } }); - - key_effect.ondestroy = () => { - for (const e of effects) { - if (e.dom) remove(e.dom); - } - }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 62ad05c573..7bfcc92005 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -20,12 +20,10 @@ export function snippet(get_snippet, node, ...args) { // Untrack so we only rerender when the snippet function itself changes, // not when an eagerly-read prop inside the snippet function changes var dom = untrack(() => /** @type {SnippetFn} */ (snippet_fn)(node, ...args)); - } - return () => { if (dom !== undefined) { - remove(dom); + return () => remove(dom); } - }; + } }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js index ff00b90127..f96e91d1ec 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,8 +1,6 @@ -import { pause_effect, render_effect } from '../../reactivity/effects.js'; -import { remove } from '../reconciler.js'; -import { current_effect } from '../../runtime.js'; +import { block, branch, pause_effect } from '../../reactivity/effects.js'; -// TODO this is very similar to `key`, can we deduplicate? +// TODO seems weird that `anchor` is unused here — possible bug? /** * @template P @@ -16,46 +14,19 @@ export function component(anchor, get_component, render_fn) { /** @type {C} */ let component; - /** @type {import('#client').Effect} */ + /** @type {import('#client').Effect | null} */ let effect; - /** - * Every time `component` changes, we create a new effect. Old effects are - * removed from this set when they have fully transitioned out - * @type {Set} - */ - let effects = new Set(); - - const component_effect = render_effect(() => { + block(() => { if (component === (component = get_component())) return; if (effect) { - var e = effect; - pause_effect(e, () => { - effects.delete(e); - }); + pause_effect(effect); + effect = null; } if (component) { - effect = render_effect(() => { - render_fn(component); - - // `render_fn` doesn't return anything, and we can't reference `effect` - // yet, so we reference it indirectly as `current_effect` - const dom = /** @type {import('#client').Effect} */ (current_effect).dom; - - return () => { - if (dom !== null) remove(dom); - }; - }, true); - - effects.add(effect); + effect = branch(() => render_fn(component)); } }); - - component_effect.ondestroy = () => { - for (const e of effects) { - if (e.dom) remove(e.dom); - } - }; } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 58645c4eac..a0c3cc6d65 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -2,12 +2,13 @@ import { namespace_svg } from '../../../../constants.js'; import { hydrate_anchor, hydrate_nodes, hydrating } from '../hydration.js'; import { empty } from '../operations.js'; import { + block, + branch, destroy_effect, pause_effect, render_effect, resume_effect } from '../../reactivity/effects.js'; -import { remove } from '../reconciler.js'; import { is_array } from '../../utils.js'; import { set_should_intro } from '../../render.js'; import { current_each_item, set_current_each_item } from './each.js'; @@ -44,110 +45,105 @@ function swap_block_dom(effect, from, to) { export function element(anchor, get_tag, is_svg, render_fn) { const parent_effect = /** @type {import('#client').Effect} */ (current_effect); - /** @type {string | null} */ - let tag; - - /** @type {string | null} */ - let current_tag; - - /** @type {null | Element} */ - let element = null; - - /** @type {import('#client').Effect | null} */ - let effect; - - /** - * The keyed `{#each ...}` item block, if any, that this element is inside. - * We track this so we can set it when changing the element, allowing any - * `animate:` directive to bind itself to the correct block - */ - let each_item_block = current_each_item; - - const wrapper = render_effect(() => { - const next_tag = get_tag() || null; - if (next_tag === tag) return; - - // See explanation of `each_item_block` above - var previous_each_item = current_each_item; - set_current_each_item(each_item_block); - - // We try our best infering the namespace in case it's not possible to determine statically, - // but on the first render on the client (without hydration) the parent will be undefined, - // since the anchor is not attached to its parent / the dom yet. - const ns = - is_svg || next_tag === 'svg' - ? namespace_svg - : is_svg === false || anchor.parentElement?.tagName === 'foreignObject' - ? null - : anchor.parentElement?.namespaceURI ?? null; - - if (effect) { - if (next_tag === null) { - // start outro - pause_effect(effect, () => { - effect = null; - current_tag = null; - element?.remove(); // TODO this should be unnecessary - }); - } else if (next_tag === current_tag) { - // same tag as is currently rendered — abort outro - resume_effect(effect); - } else { - // tag is changing — destroy immediately, render contents without intro transitions - destroy_effect(effect); - set_should_intro(false); + render_effect(() => { + /** @type {string | null} */ + let tag; + + /** @type {string | null} */ + let current_tag; + + /** @type {null | Element} */ + let element = null; + + /** @type {import('#client').Effect | null} */ + let effect; + + /** + * The keyed `{#each ...}` item block, if any, that this element is inside. + * We track this so we can set it when changing the element, allowing any + * `animate:` directive to bind itself to the correct block + */ + let each_item_block = current_each_item; + + block(() => { + const next_tag = get_tag() || null; + if (next_tag === tag) return; + + // See explanation of `each_item_block` above + var previous_each_item = current_each_item; + set_current_each_item(each_item_block); + + // We try our best infering the namespace in case it's not possible to determine statically, + // but on the first render on the client (without hydration) the parent will be undefined, + // since the anchor is not attached to its parent / the dom yet. + const ns = + is_svg || next_tag === 'svg' + ? namespace_svg + : is_svg === false || anchor.parentElement?.tagName === 'foreignObject' + ? null + : anchor.parentElement?.namespaceURI ?? null; + + if (effect) { + if (next_tag === null) { + // start outro + pause_effect(effect, () => { + effect = null; + current_tag = null; + element?.remove(); + }); + } else if (next_tag === current_tag) { + // same tag as is currently rendered — abort outro + resume_effect(effect); + } else { + // tag is changing — destroy immediately, render contents without intro transitions + destroy_effect(effect); + set_should_intro(false); + } } - } - if (next_tag && next_tag !== current_tag) { - effect = render_effect(() => { - const prev_element = element; - element = hydrating - ? /** @type {Element} */ (hydrate_nodes[0]) - : ns - ? document.createElementNS(ns, next_tag) - : document.createElement(next_tag); - - if (render_fn) { - // If hydrating, use the existing ssr comment as the anchor so that the - // inner open and close methods can pick up the existing nodes correctly - var child_anchor = hydrating - ? element.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild)) - : element.appendChild(empty()); - - if (child_anchor) { - // `child_anchor` can be undefined if this is a void element with children, - // i.e. `...`. This is - // user error, but we warn on it elsewhere (in dev) so here we just - // silently ignore it - render_fn(element, child_anchor); + if (next_tag && next_tag !== current_tag) { + effect = branch(() => { + const prev_element = element; + element = hydrating + ? /** @type {Element} */ (hydrate_nodes[0]) + : ns + ? document.createElementNS(ns, next_tag) + : document.createElement(next_tag); + + if (render_fn) { + // If hydrating, use the existing ssr comment as the anchor so that the + // inner open and close methods can pick up the existing nodes correctly + var child_anchor = hydrating + ? element.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild)) + : element.appendChild(empty()); + + if (child_anchor) { + // `child_anchor` can be undefined if this is a void element with children, + // i.e. `...`. This is + // user error, but we warn on it elsewhere (in dev) so here we just + // silently ignore it + render_fn(element, child_anchor); + } } - } - anchor.before(element); - - if (prev_element) { - swap_block_dom(parent_effect, prev_element, element); - prev_element.remove(); - } - }, true); - } + anchor.before(element); - tag = next_tag; - if (tag) current_tag = tag; - set_should_intro(true); + if (prev_element) { + swap_block_dom(parent_effect, prev_element, element); + prev_element.remove(); + } + }); + } - set_current_each_item(previous_each_item); - }); + tag = next_tag; + if (tag) current_tag = tag; + set_should_intro(true); - wrapper.ondestroy = () => { - if (element !== null) { - remove(element); - element = null; - } + set_current_each_item(previous_each_item); + }); - if (effect) { - destroy_effect(effect); - } - }; + return () => { + element?.remove(); + }; + }); } diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index dd092b092a..07d02d2c06 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -1,7 +1,6 @@ import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js'; import { empty } from '../operations.js'; -import { render_effect } from '../../reactivity/effects.js'; -import { remove } from '../reconciler.js'; +import { block } from '../../reactivity/effects.js'; /** * @param {(anchor: Node) => import('#client').Dom | void} render_fn @@ -13,6 +12,9 @@ export function head(render_fn) { let previous_hydrate_nodes = null; let was_hydrating = hydrating; + /** @type {Comment | Text} */ + var anchor; + if (hydrating) { previous_hydrate_nodes = hydrate_nodes; @@ -22,28 +24,12 @@ export function head(render_fn) { } anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(anchor)); + } else { + anchor = document.head.appendChild(empty()); } - var anchor = document.head.appendChild(empty()); - try { - /** @type {import('#client').Dom | null} */ - var dom = null; - - const head_effect = render_effect(() => { - if (dom !== null) { - remove(dom); - head_effect.dom = dom = null; - } - - dom = render_fn(anchor) ?? null; - }); - - head_effect.ondestroy = () => { - if (dom !== null) { - remove(dom); - } - }; + block(() => render_fn(anchor)); } finally { if (was_hydrating) { set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes)); diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/this.js b/packages/svelte/src/internal/client/dom/elements/bindings/this.js index c91fac434a..dfc0261e19 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/this.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/this.js @@ -1,5 +1,5 @@ import { STATE_SYMBOL } from '../../../constants.js'; -import { effect } from '../../../reactivity/effects.js'; +import { effect, render_effect } from '../../../reactivity/effects.js'; import { untrack } from '../../../runtime.js'; /** @@ -22,39 +22,38 @@ function is_bound_this(bound_value, element_or_component) { * @returns {void} */ export function bind_this(element_or_component, update, get_value, get_parts) { - /** @type {unknown[]} */ - var old_parts; + effect(() => { + /** @type {unknown[]} */ + var old_parts; - /** @type {unknown[]} */ - var parts; + /** @type {unknown[]} */ + var parts; - var e = effect(() => { - old_parts = parts; - // We only track changes to the parts, not the value itself to avoid unnecessary reruns. - parts = get_parts?.() || []; + render_effect(() => { + old_parts = parts; + // We only track changes to the parts, not the value itself to avoid unnecessary reruns. + parts = get_parts?.() || []; - untrack(() => { - if (element_or_component !== get_value(...parts)) { - update(element_or_component, ...parts); - // If this is an effect rerun (cause: each block context changes), then nullfiy the binding at - // the previous position if it isn't already taken over by a different effect. - if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) { - update(null, ...old_parts); + untrack(() => { + if (element_or_component !== get_value(...parts)) { + update(element_or_component, ...parts); + // If this is an effect rerun (cause: each block context changes), then nullfiy the binding at + // the previous position if it isn't already taken over by a different effect. + if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) { + update(null, ...old_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. - 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(() => { - if (parts && is_bound_this(get_value(...parts), element_or_component)) { - update(null, ...parts); - } - }); - }; + return () => { + // 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(() => { + if (parts && is_bound_this(get_value(...parts), element_or_component)) { + update(null, ...parts); + } + }); + }; + }); } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index b3b76d8c6a..2f1fd291c9 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -16,14 +16,16 @@ import { } from '../runtime.js'; import { DIRTY, - MANAGED, + BRANCH_EFFECT, RENDER_EFFECT, EFFECT, PRE_EFFECT, DESTROYED, INERT, IS_ELSEIF, - EFFECT_RAN + EFFECT_RAN, + BLOCK_EFFECT, + ROOT_EFFECT } from '../constants.js'; import { set } from './sources.js'; import { noop } from '../../common.js'; @@ -49,7 +51,6 @@ function create_effect(type, fn, sync, init = true) { deriveds: null, teardown: null, ctx: current_component_context, - ondestroy: null, transitions: null }; @@ -89,7 +90,7 @@ function create_effect(type, fn, sync, init = true) { * @returns {boolean} */ export function effect_active() { - return current_effect ? (current_effect.f & MANAGED) === 0 : false; + return current_effect ? (current_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 : false; } /** @@ -146,8 +147,8 @@ export function user_pre_effect(fn) { * @param {() => void | (() => void)} fn * @returns {() => void} */ -export function user_root_effect(fn) { - const effect = render_effect(fn, true); +export function effect_root(fn) { + const effect = create_effect(ROOT_EFFECT, () => untrack(fn), true); return () => { destroy_effect(effect); }; @@ -206,14 +207,20 @@ export function pre_effect(fn) { /** * @param {(() => void)} fn - * @param {boolean} managed * @returns {import('#client').Effect} */ -export function render_effect(fn, managed = false) { - let flags = RENDER_EFFECT; - if (managed) flags |= MANAGED; +export function render_effect(fn) { + return create_effect(RENDER_EFFECT, fn, true); +} + +/** @param {(() => void)} fn */ +export function block(fn) { + return create_effect(RENDER_EFFECT | BLOCK_EFFECT, fn, true); +} - return create_effect(flags, fn, true); +/** @param {(() => void)} fn */ +export function branch(fn) { + return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true); } /** @@ -237,16 +244,13 @@ export function destroy_effect(effect) { remove(effect.dom); } - effect.ondestroy?.(); - - // @ts-expect-error - effect.fn = - effect.effects = + effect.effects = effect.teardown = - effect.ondestroy = effect.ctx = effect.dom = effect.deps = + // @ts-expect-error + effect.fn = null; } @@ -328,7 +332,7 @@ function pause_children(effect, transitions, local) { if (effect.effects !== null) { for (const child of effect.effects) { - var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & MANAGED) !== 0; + var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & BRANCH_EFFECT) !== 0; pause_children(child, transitions, transparent ? local : false); } } @@ -359,7 +363,7 @@ function resume_children(effect, local) { if (effect.effects !== null) { for (const child of effect.effects) { - var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & MANAGED) !== 0; + var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & BRANCH_EFFECT) !== 0; resume_children(child, transparent ? local : false); } } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 64113bfff4..1f1c1814e8 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -18,7 +18,7 @@ import { untrack } from '../runtime.js'; import { equals, safe_equals } from './equality.js'; -import { CLEAN, DERIVED, DIRTY, MANAGED, UNINITIALIZED } from '../constants.js'; +import { CLEAN, DERIVED, DIRTY, BRANCH_EFFECT, UNINITIALIZED } from '../constants.js'; /** * @template V @@ -131,7 +131,7 @@ export function set(signal, value) { initialized && current_effect !== null && (current_effect.f & CLEAN) !== 0 && - (current_effect.f & MANAGED) === 0 + (current_effect.f & BRANCH_EFFECT) === 0 ) { if (current_dependencies !== null && current_dependencies.includes(signal)) { set_signal_status(current_effect, DIRTY); diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index a28b0fefbc..2b2563dbf3 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -40,8 +40,6 @@ export interface Effect extends Reaction { dom: Dom | null; /** The associated component context */ ctx: null | ComponentContext; - /** Stuff to do when the effect is destroyed */ - ondestroy: null | (() => void); /** The effect function */ fn: () => void | (() => void); /** The teardown function returned from the effect function */ diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 65fe8a9a63..c7d8de430a 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -7,9 +7,8 @@ import { init_operations } from './dom/operations.js'; import { PassiveDelegatedEvents } from '../../constants.js'; -import { remove } from './dom/reconciler.js'; -import { flush_sync, push, pop, current_component_context } from './runtime.js'; -import { render_effect, destroy_effect } from './reactivity/effects.js'; +import { flush_sync, push, pop, current_component_context, untrack } from './runtime.js'; +import { effect_root, branch } from './reactivity/effects.js'; import { hydrate_anchor, hydrate_nodes, @@ -189,48 +188,20 @@ export function hydrate(component, options) { * events?: { [Property in keyof Events]: (e: Events[Property]) => any }; * context?: Map; * intro?: boolean; - * recover?: false; * }} options * @returns {Exports} */ -function _mount(Component, options) { +function _mount( + Component, + { target, anchor, props = /** @type {Props} */ ({}), events, context, intro = false } +) { init_operations(); const registered_events = new Set(); - const container = options.target; - - should_intro = options.intro ?? false; - /** @type {Exports} */ - // @ts-expect-error will be defined because the render effect runs synchronously - let component = undefined; - - const effect = render_effect(() => { - if (options.context) { - push({}); - /** @type {import('../client/types.js').ComponentContext} */ (current_component_context).c = - options.context; - } - if (!options.props) { - options.props = /** @type {Props} */ ({}); - } - if (options.events) { - // We can't spread the object or else we'd lose the state proxy stuff, if it is one - /** @type {any} */ (options.props).$$events = options.events; - } - component = - // @ts-expect-error the public typings are not what the actual function looks like - Component(options.anchor, options.props) || {}; - if (options.context) { - pop(); - } - }, true); - - const bound_event_listener = handle_event_propagation.bind(null, container); + const bound_event_listener = handle_event_propagation.bind(null, target); const bound_document_event_listener = handle_event_propagation.bind(null, document); - should_intro = true; - /** @param {Array} events */ const event_handle = (events) => { for (let i = 0; i < events.length; i++) { @@ -240,7 +211,7 @@ function _mount(Component, options) { // Add the event listener to both the container and the document. // The container listener ensures we catch events from within in case // the outer content stops propagation of the event. - container.addEventListener( + target.addEventListener( event_name, bound_event_listener, PassiveDelegatedEvents.includes(event_name) @@ -263,21 +234,48 @@ function _mount(Component, options) { } } }; + event_handle(array_from(all_registered_events)); root_event_handles.add(event_handle); - mounted_components.set(component, () => { - for (const event_name of registered_events) { - container.removeEventListener(event_name, bound_event_listener); - } - root_event_handles.delete(event_handle); - const dom = effect.dom; - if (dom !== null) { - remove(dom); - } - destroy_effect(effect); + /** @type {Exports} */ + // @ts-expect-error will be defined because the render effect runs synchronously + let component = undefined; + + const unmount = effect_root(() => { + branch(() => { + untrack(() => { + if (context) { + push({}); + var ctx = /** @type {import('#client').ComponentContext} */ (current_component_context); + ctx.c = context; + } + + if (events) { + // We can't spread the object or else we'd lose the state proxy stuff, if it is one + /** @type {any} */ (props).$$events = events; + } + + should_intro = intro; + // @ts-expect-error the public typings are not what the actual function looks like + component = Component(anchor, props) || {}; + should_intro = true; + + if (context) { + pop(); + } + }); + }); + + return () => { + for (const event_name of registered_events) { + target.removeEventListener(event_name, bound_event_listener); + } + root_event_handles.delete(event_handle); + }; }); + mounted_components.set(component, unmount); return component; } diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 707f4d9619..bf80aa2cfe 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -20,8 +20,10 @@ import { UNOWNED, DESTROYED, INERT, - MANAGED, - STATE_SYMBOL + BRANCH_EFFECT, + STATE_SYMBOL, + BLOCK_EFFECT, + ROOT_EFFECT } from './constants.js'; import { flush_tasks } from './dom/task.js'; import { add_owner } from './dev/ownership.js'; @@ -359,7 +361,9 @@ export function destroy_children(signal) { if (signal.effects) { for (var i = 0; i < signal.effects.length; i += 1) { var effect = signal.effects[i]; - if ((effect.f & MANAGED) === 0) { + + // TODO ideally root effects would be parentless + if ((effect.f & ROOT_EFFECT) === 0) { destroy_effect(effect); } } @@ -379,7 +383,9 @@ export function destroy_children(signal) { * @returns {void} */ export function execute_effect(effect) { - if ((effect.f & DESTROYED) !== 0) { + var flags = effect.f; + + if ((flags & DESTROYED) !== 0) { return; } @@ -394,7 +400,10 @@ export function execute_effect(effect) { current_component_context = component_context; try { - destroy_children(effect); + if ((flags & BLOCK_EFFECT) === 0) { + destroy_children(effect); + } + effect.teardown?.(); var teardown = execute_reaction_fn(effect); effect.teardown = typeof teardown === 'function' ? teardown : null; @@ -404,7 +413,7 @@ export function execute_effect(effect) { } const parent = effect.parent; - if ((effect.f & PRE_EFFECT) !== 0 && parent !== null) { + if ((flags & PRE_EFFECT) !== 0 && parent !== null) { flush_local_pre_effects(parent); } } @@ -488,7 +497,7 @@ export function schedule_effect(signal) { // before this effect is scheduled. We know they will be destroyed // so we can make them inert to avoid having to find them in the // queue and remove them. - if ((flags & MANAGED) === 0) { + if ((flags & BRANCH_EFFECT) === 0) { mark_subtree_children_inert(signal, true); } } else { @@ -709,7 +718,11 @@ export function get(signal) { } // Register the dependency on the current reaction signal. - if (current_reaction !== null && (current_reaction.f & MANAGED) === 0 && !current_untracking) { + if ( + current_reaction !== null && + (current_reaction.f & BRANCH_EFFECT) === 0 && + !current_untracking + ) { const unowned = (current_reaction.f & UNOWNED) !== 0; const dependencies = current_reaction.deps; if ( @@ -734,7 +747,7 @@ export function get(signal) { current_untracked_writes !== null && current_effect !== null && (current_effect.f & CLEAN) !== 0 && - (current_effect.f & MANAGED) === 0 && + (current_effect.f & BRANCH_EFFECT) === 0 && current_untracked_writes.includes(signal) ) { set_signal_status(current_effect, DIRTY); diff --git a/packages/svelte/src/reactivity/map.test.ts b/packages/svelte/src/reactivity/map.test.ts index 979e8ded05..e758f453c5 100644 --- a/packages/svelte/src/reactivity/map.test.ts +++ b/packages/svelte/src/reactivity/map.test.ts @@ -1,4 +1,4 @@ -import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js'; +import { pre_effect, effect_root } from '../internal/client/reactivity/effects.js'; import { flushSync } from '../main/main-client.js'; import { ReactiveMap } from './map.js'; import { assert, test } from 'vitest'; @@ -14,7 +14,7 @@ test('map.values()', () => { const log: any = []; - const cleanup = user_root_effect(() => { + const cleanup = effect_root(() => { pre_effect(() => { log.push(map.size); }); @@ -50,7 +50,7 @@ test('map.get(...)', () => { const log: any = []; - const cleanup = user_root_effect(() => { + const cleanup = effect_root(() => { pre_effect(() => { log.push('get 1', map.get(1)); }); @@ -86,7 +86,7 @@ test('map.has(...)', () => { const log: any = []; - const cleanup = user_root_effect(() => { + const cleanup = effect_root(() => { pre_effect(() => { log.push('has 1', map.has(1)); }); @@ -129,7 +129,7 @@ test('map handling of undefined values', () => { const log: any = []; - const cleanup = user_root_effect(() => { + const cleanup = effect_root(() => { map.set(1, undefined); pre_effect(() => { diff --git a/packages/svelte/src/reactivity/set.test.ts b/packages/svelte/src/reactivity/set.test.ts index e93ac4e565..def9c64d3b 100644 --- a/packages/svelte/src/reactivity/set.test.ts +++ b/packages/svelte/src/reactivity/set.test.ts @@ -1,4 +1,4 @@ -import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js'; +import { pre_effect, effect_root } from '../internal/client/reactivity/effects.js'; import { flushSync } from '../main/main-client.js'; import { ReactiveSet } from './set.js'; import { assert, test } from 'vitest'; @@ -8,7 +8,7 @@ test('set.values()', () => { const log: any = []; - const cleanup = user_root_effect(() => { + const cleanup = effect_root(() => { pre_effect(() => { log.push(set.size); }); @@ -40,7 +40,7 @@ test('set.has(...)', () => { const log: any = []; - const cleanup = user_root_effect(() => { + const cleanup = effect_root(() => { pre_effect(() => { log.push('has 1', set.has(1)); }); diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index f3a3054d28..ef2bc96fc3 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -24,7 +24,7 @@ function run_test(runes: boolean, fn: (runes: boolean) => () => void) { let execute: any; const signal = render_effect(() => { execute = fn(runes); - }, true); + }); $.pop(); execute(); destroy_effect(signal);