From fbb7da7e354a3d7b739d1a31fe2e295204b110b8 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 2 Jul 2024 09:11:00 -0700 Subject: [PATCH] feat: simpler effect DOM boundaries (#12258) * simpler effect dom boundaries * remove unused argument * tidy up * simplify * skip redundant comment templates for components (and others TODO) * same optimisation for render tags * DRY out * appease typescript * changeset * tighten up, leave note to self * reinstate $.comment optimisation * add explanation * comments --- .changeset/beige-gifts-appear.md | 5 + .../3-transform/client/visitors/template.js | 61 +++---- .../3-transform/server/transform-server.js | 22 ++- .../phases/3-transform/server/types.d.ts | 1 + .../src/compiler/phases/3-transform/utils.js | 152 ++++++++++-------- packages/svelte/src/constants.js | 1 - .../svelte/src/internal/client/dev/hmr.js | 3 +- .../src/internal/client/dom/blocks/await.js | 2 +- .../src/internal/client/dom/blocks/each.js | 17 +- .../src/internal/client/dom/blocks/html.js | 2 +- .../src/internal/client/dom/blocks/if.js | 4 +- .../src/internal/client/dom/blocks/key.js | 2 +- .../src/internal/client/dom/blocks/snippet.js | 4 +- .../client/dom/blocks/svelte-component.js | 12 +- .../client/dom/blocks/svelte-element.js | 2 +- .../internal/client/dom/blocks/svelte-head.js | 2 +- .../src/internal/client/dom/operations.js | 7 +- .../src/internal/client/dom/template.js | 58 +++---- .../src/internal/client/reactivity/effects.js | 42 +---- .../src/internal/client/reactivity/types.d.ts | 17 +- packages/svelte/src/internal/client/render.js | 2 +- .../_expected/client/index.svelte.js | 2 +- .../_expected/client/index.svelte.js | 6 +- .../_expected/server/index.svelte.js | 2 - .../_expected/client/index.svelte.js | 6 +- .../_expected/server/index.svelte.js | 4 - 26 files changed, 203 insertions(+), 235 deletions(-) create mode 100644 .changeset/beige-gifts-appear.md diff --git a/.changeset/beige-gifts-appear.md b/.changeset/beige-gifts-appear.md new file mode 100644 index 0000000000..8dfd043c64 --- /dev/null +++ b/.changeset/beige-gifts-appear.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: simpler effect DOM boundaries diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 24884305d8..2a74aafc58 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -36,7 +36,6 @@ import { EACH_KEYED, is_capture_event, TEMPLATE_FRAGMENT, - TEMPLATE_UNSET_START, TEMPLATE_USE_IMPORT_NODE, TRANSITION_GLOBAL, TRANSITION_IN, @@ -1561,7 +1560,7 @@ export const template_visitors = { const namespace = infer_namespace(context.state.metadata.namespace, parent, node.nodes); - const { hoisted, trimmed } = clean_nodes( + const { hoisted, trimmed, is_standalone } = clean_nodes( parent, node.nodes, context.path, @@ -1676,56 +1675,38 @@ export const template_visitors = { ); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else { - /** @type {(is_text: boolean) => import('estree').Expression} */ - const expression = (is_text) => - is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id); - - process_children(trimmed, expression, false, { ...context, state }); - - var first = trimmed[0]; - - /** - * If the first item in an effect is a static slot or render tag, it will clone - * a template but without creating a child effect. In these cases, we need to keep - * the current `effect.nodes.start` undefined, so that it can be populated by - * the item in question - * TODO come up with a better name than `unset` - */ - var unset = false; - - if (first.type === 'SlotElement') unset = true; - if (first.type === 'RenderTag' && !first.metadata.dynamic) unset = true; - if (first.type === 'Component' && !first.metadata.dynamic && !context.state.options.hmr) { - unset = true; - } + if (is_standalone) { + // no need to create a template, we can just use the existing block's anchor + process_children(trimmed, () => b.id('$$anchor'), false, { ...context, state }); + } else { + /** @type {(is_text: boolean) => import('estree').Expression} */ + const expression = (is_text) => + is_text ? b.call('$.first_child', id, b.true) : b.call('$.first_child', id); - const use_comment_template = state.template.length === 1 && state.template[0] === ''; + process_children(trimmed, expression, false, { ...context, state }); - if (use_comment_template) { - // special case — we can use `$.comment` instead of creating a unique template - body.push(b.var(id, b.call('$.comment', unset && b.literal(unset)))); - } else { let flags = TEMPLATE_FRAGMENT; - if (unset) { - flags |= TEMPLATE_UNSET_START; - } - if (state.metadata.context.template_needs_import_node) { flags |= TEMPLATE_USE_IMPORT_NODE; } - add_template(template_name, [ - b.template([b.quasi(state.template.join(''), true)], []), - b.literal(flags) - ]); + if (state.template.length === 1 && state.template[0] === '') { + // special case — we can use `$.comment` instead of creating a unique template + body.push(b.var(id, b.call('$.comment'))); + } else { + add_template(template_name, [ + b.template([b.quasi(state.template.join(''), true)], []), + b.literal(flags) + ]); + + body.push(b.var(id, b.call(template_name))); + } - body.push(b.var(id, b.call(template_name))); + close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } body.push(...state.before_init, ...state.init); - - close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } } else { body.push(...state.before_init, ...state.init); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js index 5b0f16db9d..b9d6a5bc4d 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/transform-server.js @@ -1002,6 +1002,8 @@ function serialize_inline_component(node, expression, context) { ) ); + context.state.template.push(statement); + } else if (context.state.skip_hydration_boundaries) { context.state.template.push(statement); } else { context.state.template.push(block_open, statement, block_close); @@ -1112,7 +1114,7 @@ const template_visitors = { const parent = context.path.at(-1) ?? node; const namespace = infer_namespace(context.state.namespace, parent, node.nodes); - const { hoisted, trimmed } = clean_nodes( + const { hoisted, trimmed, is_standalone } = clean_nodes( parent, node.nodes, context.path, @@ -1127,7 +1129,8 @@ const template_visitors = { ...context.state, init: [], template: [], - namespace + namespace, + skip_hydration_boundaries: is_standalone }; for (const node of hoisted) { @@ -1180,17 +1183,23 @@ const template_visitors = { return /** @type {import('estree').Expression} */ (context.visit(arg)); }); + if (!context.state.skip_hydration_boundaries) { + context.state.template.push(block_open); + } + context.state.template.push( - block_open, b.stmt( (node.expression.type === 'CallExpression' ? b.call : b.maybe_call)( snippet_function, b.id('$$payload'), ...snippet_args ) - ), - block_close + ) ); + + if (!context.state.skip_hydration_boundaries) { + context.state.template.push(block_close); + } }, ClassDirective() { throw new Error('Node should have been handled elsewhere'); @@ -1925,7 +1934,8 @@ export function server_component(analysis, options) { template: /** @type {any} */ (null), namespace: options.namespace, preserve_whitespace: options.preserveWhitespace, - private_derived: new Map() + private_derived: new Map(), + skip_hydration_boundaries: false }; const module = /** @type {import('estree').Program} */ ( diff --git a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts index c3409984fe..207d712d67 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts +++ b/packages/svelte/src/compiler/phases/3-transform/server/types.d.ts @@ -22,6 +22,7 @@ export interface ComponentServerTransformState extends ServerTransformState { readonly template: Array; readonly namespace: Namespace; readonly preserve_whitespace: boolean; + readonly skip_hydration_boundaries: boolean; } export type Context = import('zimmerframe').Context; diff --git a/packages/svelte/src/compiler/phases/3-transform/utils.js b/packages/svelte/src/compiler/phases/3-transform/utils.js index 9359bfebcb..0ade9b427e 100644 --- a/packages/svelte/src/compiler/phases/3-transform/utils.js +++ b/packages/svelte/src/compiler/phases/3-transform/utils.js @@ -185,83 +185,105 @@ export function clean_nodes( } } - if (preserve_whitespace) { - return { hoisted, trimmed: regular }; - } + let trimmed = regular; - let first, last; + if (!preserve_whitespace) { + trimmed = []; - while ((first = regular[0]) && first.type === 'Text' && !regex_not_whitespace.test(first.data)) { - regular.shift(); - } + let first, last; - if (first?.type === 'Text') { - first.raw = first.raw.replace(regex_starts_with_whitespaces, ''); - first.data = first.data.replace(regex_starts_with_whitespaces, ''); - } + while ( + (first = regular[0]) && + first.type === 'Text' && + !regex_not_whitespace.test(first.data) + ) { + regular.shift(); + } - while ((last = regular.at(-1)) && last.type === 'Text' && !regex_not_whitespace.test(last.data)) { - regular.pop(); - } + if (first?.type === 'Text') { + first.raw = first.raw.replace(regex_starts_with_whitespaces, ''); + first.data = first.data.replace(regex_starts_with_whitespaces, ''); + } - if (last?.type === 'Text') { - last.raw = last.raw.replace(regex_ends_with_whitespaces, ''); - last.data = last.data.replace(regex_ends_with_whitespaces, ''); - } + while ( + (last = regular.at(-1)) && + last.type === 'Text' && + !regex_not_whitespace.test(last.data) + ) { + regular.pop(); + } - const can_remove_entirely = - (namespace === 'svg' && - (parent.type !== 'RegularElement' || parent.name !== 'text') && - !path.some((n) => n.type === 'RegularElement' && n.name === 'text')) || - (parent.type === 'RegularElement' && - // TODO others? - (parent.name === 'select' || - parent.name === 'tr' || - parent.name === 'table' || - parent.name === 'tbody' || - parent.name === 'thead' || - parent.name === 'tfoot' || - parent.name === 'colgroup' || - parent.name === 'datalist')); + if (last?.type === 'Text') { + last.raw = last.raw.replace(regex_ends_with_whitespaces, ''); + last.data = last.data.replace(regex_ends_with_whitespaces, ''); + } - /** @type {Compiler.SvelteNode[]} */ - const trimmed = []; - - // Replace any whitespace between a text and non-text node with a single spaceand keep whitespace - // as-is within text nodes, or between text nodes and expression tags (because in the end they count - // as one text). This way whitespace is mostly preserved when using CSS with `white-space: pre-line` - // and default slot content going into a pre tag (which we can't see). - for (let i = 0; i < regular.length; i++) { - const prev = regular[i - 1]; - const node = regular[i]; - const next = regular[i + 1]; - - if (node.type === 'Text') { - if (prev?.type !== 'ExpressionTag') { - const prev_is_text_ending_with_whitespace = - prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data); - node.data = node.data.replace( - regex_starts_with_whitespaces, - prev_is_text_ending_with_whitespace ? '' : ' ' - ); - node.raw = node.raw.replace( - regex_starts_with_whitespaces, - prev_is_text_ending_with_whitespace ? '' : ' ' - ); - } - if (next?.type !== 'ExpressionTag') { - node.data = node.data.replace(regex_ends_with_whitespaces, ' '); - node.raw = node.raw.replace(regex_ends_with_whitespaces, ' '); - } - if (node.data && (node.data !== ' ' || !can_remove_entirely)) { + const can_remove_entirely = + (namespace === 'svg' && + (parent.type !== 'RegularElement' || parent.name !== 'text') && + !path.some((n) => n.type === 'RegularElement' && n.name === 'text')) || + (parent.type === 'RegularElement' && + // TODO others? + (parent.name === 'select' || + parent.name === 'tr' || + parent.name === 'table' || + parent.name === 'tbody' || + parent.name === 'thead' || + parent.name === 'tfoot' || + parent.name === 'colgroup' || + parent.name === 'datalist')); + + // Replace any whitespace between a text and non-text node with a single spaceand keep whitespace + // as-is within text nodes, or between text nodes and expression tags (because in the end they count + // as one text). This way whitespace is mostly preserved when using CSS with `white-space: pre-line` + // and default slot content going into a pre tag (which we can't see). + for (let i = 0; i < regular.length; i++) { + const prev = regular[i - 1]; + const node = regular[i]; + const next = regular[i + 1]; + + if (node.type === 'Text') { + if (prev?.type !== 'ExpressionTag') { + const prev_is_text_ending_with_whitespace = + prev?.type === 'Text' && regex_ends_with_whitespaces.test(prev.data); + node.data = node.data.replace( + regex_starts_with_whitespaces, + prev_is_text_ending_with_whitespace ? '' : ' ' + ); + node.raw = node.raw.replace( + regex_starts_with_whitespaces, + prev_is_text_ending_with_whitespace ? '' : ' ' + ); + } + if (next?.type !== 'ExpressionTag') { + node.data = node.data.replace(regex_ends_with_whitespaces, ' '); + node.raw = node.raw.replace(regex_ends_with_whitespaces, ' '); + } + if (node.data && (node.data !== ' ' || !can_remove_entirely)) { + trimmed.push(node); + } + } else { trimmed.push(node); } - } else { - trimmed.push(node); } } - return { hoisted, trimmed }; + var first = trimmed[0]; + + /** + * In a case like `{#if x}{/if}`, we don't need to wrap the child in + * comments — we can just use the parent block's anchor for the component. + * TODO extend this optimisation to other cases + */ + const is_standalone = + trimmed.length === 1 && + ((first.type === 'RenderTag' && !first.metadata.dynamic) || + (first.type === 'Component' && + !first.attributes.some( + (attribute) => attribute.type === 'Attribute' && attribute.name.startsWith('--') + ))); + + return { hoisted, trimmed, is_standalone }; } /** diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 5c22be5f5d..a8963d6854 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -18,7 +18,6 @@ export const TRANSITION_GLOBAL = 1 << 2; export const TEMPLATE_FRAGMENT = 1; export const TEMPLATE_USE_IMPORT_NODE = 1 << 1; -export const TEMPLATE_UNSET_START = 1 << 2; export const HYDRATION_START = '['; export const HYDRATION_END = ']'; diff --git a/packages/svelte/src/internal/client/dev/hmr.js b/packages/svelte/src/internal/client/dev/hmr.js index f2be7e57f6..20f205ade8 100644 --- a/packages/svelte/src/internal/client/dev/hmr.js +++ b/packages/svelte/src/internal/client/dev/hmr.js @@ -1,4 +1,5 @@ /** @import { Source, Effect } from '#client' */ +import { empty } from '../dom/operations.js'; import { block, branch, destroy_effect } from '../reactivity/effects.js'; import { set_should_intro } from '../render.js'; import { get } from '../runtime.js'; @@ -19,7 +20,7 @@ export function hmr(source) { /** @type {Effect} */ let effect; - block(anchor, 0, () => { + block(() => { const component = get(source); if (effect) { diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index d1681d3e00..4c57f9b564 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -105,7 +105,7 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) { } } - var effect = block(anchor, 0, () => { + var effect = block(() => { if (input === (input = get_input())) return; if (is_promise(input)) { diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index 7fdd2fc0e7..e6146b2f23 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -24,8 +24,7 @@ import { run_out_transitions, pause_children, pause_effect, - resume_effect, - get_first_node + resume_effect } from '../../reactivity/effects.js'; import { source, mutable_source, set } from '../../reactivity/sources.js'; import { is_array, is_frozen } from '../../utils.js'; @@ -124,7 +123,7 @@ export function each(anchor, flags, get_collection, get_key, render_fn, fallback /** @type {import('#client').Effect | null} */ var fallback = null; - block(anchor, 0, () => { + block(() => { var collection = get_collection(); var array = is_array(collection) @@ -294,7 +293,9 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { item = items.get(key); if (item === undefined) { - var child_anchor = current ? get_first_node(current.e) : anchor; + var child_anchor = current + ? /** @type {import('#client').EffectNodes} */ (current.e.nodes).start + : anchor; prev = create_item( child_anchor, @@ -515,10 +516,12 @@ function create_item(anchor, state, prev, next, value, key, index, render_fn, fl * @param {Text | Element | Comment} anchor */ function move(item, next, anchor) { - var end = item.next ? get_first_node(item.next.e) : anchor; - var dest = next ? get_first_node(next.e) : anchor; + var end = item.next + ? /** @type {import('#client').EffectNodes} */ (item.next.e.nodes).start + : anchor; - var node = get_first_node(item.e); + var dest = next ? /** @type {import('#client').EffectNodes} */ (next.e.nodes).start : anchor; + var node = /** @type {import('#client').EffectNodes} */ (item.e.nodes).start; while (node !== end) { var next_node = /** @type {import('#client').TemplateNode} */ (node.nextSibling); diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index c014da4a1d..55ead2600a 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -16,7 +16,7 @@ export function html(anchor, get_value, svg, mathml) { /** @type {import('#client').Effect | null} */ var effect; - block(anchor, 0, () => { + block(() => { if (value === (value = get_value())) return; if (effect) { diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 9ad208fe2f..80ed8c09f4 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -30,7 +30,7 @@ export function if_block( var flags = elseif ? EFFECT_TRANSPARENT : 0; - block(anchor, flags, () => { + 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 */ @@ -78,5 +78,5 @@ export function if_block( // continue in hydration mode set_hydrating(true); } - }); + }, flags); } diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index ae55847f80..489262b714 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -17,7 +17,7 @@ export function key_block(anchor, get_key, render_fn) { /** @type {Effect} */ let effect; - block(anchor, 0, () => { + block(() => { if (safe_not_equal(key, (key = get_key()))) { if (effect) { pause_effect(effect); diff --git a/packages/svelte/src/internal/client/dom/blocks/snippet.js b/packages/svelte/src/internal/client/dom/blocks/snippet.js index 5602f7baf2..5bdc4e6a6d 100644 --- a/packages/svelte/src/internal/client/dom/blocks/snippet.js +++ b/packages/svelte/src/internal/client/dom/blocks/snippet.js @@ -20,7 +20,7 @@ export function snippet(anchor, get_snippet, ...args) { /** @type {import('#client').Effect | null} */ var snippet_effect; - block(anchor, EFFECT_TRANSPARENT, () => { + block(() => { if (snippet === (snippet = get_snippet())) return; if (snippet_effect) { @@ -31,7 +31,7 @@ export function snippet(anchor, get_snippet, ...args) { if (snippet) { snippet_effect = branch(() => /** @type {SnippetFn} */ (snippet)(anchor, ...args)); } - }); + }, EFFECT_TRANSPARENT); } /** 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 30ace817f6..9a4f6db334 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-component.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-component.js @@ -1,7 +1,5 @@ /** @import { TemplateNode, Dom, Effect } from '#client' */ -import { DEV } from 'esm-env'; import { block, branch, pause_effect } from '../../reactivity/effects.js'; -import { empty } from '../operations.js'; /** * @template P @@ -18,12 +16,7 @@ export function component(anchor, get_component, render_fn) { /** @type {Effect | null} */ let effect; - var component_anchor = anchor; - - // create a dummy anchor for the HMR wrapper, if such there be - if (DEV) component_anchor = empty(); - - block(anchor, 0, () => { + block(() => { if (component === (component = get_component())) return; if (effect) { @@ -32,8 +25,7 @@ export function component(anchor, get_component, render_fn) { } if (component) { - if (DEV) anchor.before(component_anchor); - effect = branch(() => render_fn(component_anchor, component)); + effect = branch(() => render_fn(anchor, component)); } }); } 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 9fc0ab1dbb..ba9e1e7f0f 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -48,7 +48,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio */ let each_item_block = current_each_item; - block(anchor, 0, () => { + block(() => { const next_tag = get_tag() || null; const ns = get_namespace ? get_namespace() 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 d61c8fbe24..f25ecefa03 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -48,7 +48,7 @@ export function head(render_fn) { } try { - block(null, HEAD_EFFECT, () => render_fn(anchor)); + block(() => render_fn(anchor), HEAD_EFFECT); } finally { if (was_hydrating) { set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes)); diff --git a/packages/svelte/src/internal/client/dom/operations.js b/packages/svelte/src/internal/client/dom/operations.js index 78c0969088..a8f3b096a8 100644 --- a/packages/svelte/src/internal/client/dom/operations.js +++ b/packages/svelte/src/internal/client/dom/operations.js @@ -78,7 +78,12 @@ export function child(node) { export function first_child(fragment, is_text) { if (!hydrating) { // when not hydrating, `fragment` is a `DocumentFragment` (the result of calling `open_frag`) - return /** @type {DocumentFragment} */ (fragment).firstChild; + var first = /** @type {DocumentFragment} */ (fragment).firstChild; + + // TODO prevent user comments with the empty string when preserveComments is true + if (first instanceof Comment && first.data === '') return first.nextSibling; + + return first; } // if an {expression} is empty during SSR, there might be no diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 1ebef63bba..ab7c57fa71 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -2,27 +2,15 @@ import { get_start, hydrate_nodes, hydrate_start, hydrating } from './hydration. import { empty } from './operations.js'; import { create_fragment_from_html } from './reconciler.js'; import { current_effect } from '../runtime.js'; -import { - TEMPLATE_FRAGMENT, - TEMPLATE_UNSET_START, - TEMPLATE_USE_IMPORT_NODE -} from '../../../constants.js'; +import { TEMPLATE_FRAGMENT, TEMPLATE_USE_IMPORT_NODE } from '../../../constants.js'; import { queue_micro_task } from './task.js'; /** - * - * @param {import('#client').TemplateNode | undefined | null} start + * @param {import('#client').TemplateNode} start * @param {import('#client').TemplateNode} end - * @param {import('#client').TemplateNode | null} anchor */ -export function assign_nodes(start, end, anchor = null) { - const effect = /** @type {import('#client').Effect} */ (current_effect); - - if (effect.nodes === null) { - effect.nodes = { start, anchor, end }; - } else if (effect.nodes.start === undefined) { - effect.nodes.start = start; - } +export function assign_nodes(start, end) { + /** @type {import('#client').Effect} */ (current_effect).nodes ??= { start, end }; } /** @@ -38,8 +26,11 @@ export function template(content, flags) { /** @type {Node} */ var node; + /** + * Whether or not the first item is a text/element node. If not, we need to + * create an additional comment node to act as `effect.nodes.start` + */ var has_start = !content.startsWith(''); - var unset = (flags & TEMPLATE_UNSET_START) !== 0; return () => { if (hydrating) { @@ -49,7 +40,7 @@ export function template(content, flags) { } if (!node) { - node = create_fragment_from_html(content); + node = create_fragment_from_html(has_start ? content : '' + content); if (!is_fragment) node = /** @type {Node} */ (node.firstChild); } @@ -58,11 +49,10 @@ export function template(content, flags) { ); if (is_fragment) { - var first = /** @type {import('#client').TemplateNode} */ (clone.firstChild); - var start = has_start ? first : unset ? undefined : null; + var start = /** @type {import('#client').TemplateNode} */ (clone.firstChild); var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild); - assign_nodes(start, end, first); + assign_nodes(start, end); } else { assign_nodes(clone, clone); } @@ -103,15 +93,18 @@ export function template_with_script(content, flags) { */ /*#__NO_SIDE_EFFECTS__*/ export function ns_template(content, flags, ns = 'svg') { + /** + * Whether or not the first item is a text/element node. If not, we need to + * create an additional comment node to act as `effect.nodes.start` + */ + var has_start = !content.startsWith(''); + var is_fragment = (flags & TEMPLATE_FRAGMENT) !== 0; - var wrapped = `<${ns}>${content}`; + var wrapped = `<${ns}>${has_start ? content : '' + content}`; /** @type {Element | DocumentFragment} */ var node; - var has_start = !content.startsWith(''); - var unset = (flags & TEMPLATE_UNSET_START) !== 0; - return () => { if (hydrating) { assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]); @@ -136,11 +129,10 @@ export function ns_template(content, flags, ns = 'svg') { var clone = /** @type {import('#client').TemplateNode} */ (node.cloneNode(true)); if (is_fragment) { - var first = /** @type {import('#client').TemplateNode} */ (clone.firstChild); - var start = has_start ? first : unset ? undefined : null; + var start = /** @type {import('#client').TemplateNode} */ (clone.firstChild); var end = /** @type {import('#client').TemplateNode} */ (clone.lastChild); - assign_nodes(start, end, first); + assign_nodes(start, end); } else { assign_nodes(clone, clone); } @@ -238,10 +230,7 @@ export function text(anchor) { return node; } -/** - * @param {boolean} unset - */ -export function comment(unset = false) { +export function comment() { // we're not delegating to `template` here for performance reasons if (hydrating) { assign_nodes(get_start(), hydrate_nodes[hydrate_nodes.length - 1]); @@ -250,10 +239,11 @@ export function comment(unset = false) { } var frag = document.createDocumentFragment(); + var start = document.createComment(''); var anchor = empty(); - frag.append(anchor); + frag.append(start, anchor); - assign_nodes(unset ? undefined : null, anchor, anchor); + assign_nodes(start, anchor); return frag; } diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 3e2db129b9..1ff7750dad 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -299,14 +299,11 @@ export function template_effect(fn) { } /** - * @param {import('#client').TemplateNode | null} anchor - * @param {number} flags * @param {(() => void)} fn + * @param {number} flags */ -export function block(anchor, flags, fn) { - const effect = create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); - if (anchor !== null) effect.nodes = { start: null, anchor: null, end: anchor }; - return effect; +export function block(fn, flags = 0) { + return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true); } /** @@ -346,7 +343,7 @@ export function destroy_effect(effect, remove_dom = true) { if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes !== null) { /** @type {import('#client').TemplateNode | null} */ - var node = get_first_node(effect); + var node = effect.nodes.start; var end = effect.nodes.end; while (node !== null) { @@ -392,37 +389,6 @@ export function destroy_effect(effect, remove_dom = true) { null; } -/** - * @param {import('#client').Effect} effect - * @returns {import('#client').TemplateNode} - */ -export function get_first_node(effect) { - var nodes = /** @type {NonNullable} */ (effect.nodes); - var start = nodes.start; - - if (start === undefined) { - // edge case — a snippet or component was the first item inside the effect, - // but it didn't render any DOM. in this case, we return the item's anchor - return /** @type {import('#client').TemplateNode} */ (nodes.anchor); - } - - if (start !== null) { - return start; - } - - var child = effect.first; - while (child && (child.nodes === null || (child.f & HEAD_EFFECT) !== 0)) { - child = child.next; - } - - if (child !== null && child.nodes !== null) { - return get_first_node(child); - } - - // in the case that there's no DOM, return the first anchor - return nodes.end; -} - /** * Detach an effect from the effect tree, freeing up memory and * reducing the amount of work that happens on subsequent traversals diff --git a/packages/svelte/src/internal/client/reactivity/types.d.ts b/packages/svelte/src/internal/client/reactivity/types.d.ts index 1d35446607..e2c2e9899e 100644 --- a/packages/svelte/src/internal/client/reactivity/types.d.ts +++ b/packages/svelte/src/internal/client/reactivity/types.d.ts @@ -34,13 +34,20 @@ export interface Derived extends Value, Reaction { deriveds: null | Derived[]; } +export interface EffectNodes { + start: TemplateNode; + end: TemplateNode; +} + export interface Effect extends Reaction { parent: Effect | null; - nodes: null | { - start: undefined | null | TemplateNode; - anchor: null | TemplateNode; - end: TemplateNode; - }; + /** + * Branch effects store their start/end nodes so that they can be + * removed when the effect is destroyed, or moved when an `each` + * block is reconciled. In the case of a single text/element node, + * `start` and `end` will be the same. + */ + nodes: null | EffectNodes; /** The associated component context */ ctx: null | ComponentContext; /** The effect function */ diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 1944f6396e..1c1dd6d7f0 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -1,7 +1,7 @@ import { DEV } from 'esm-env'; import { clear_text_content, create_element, empty, init_operations } from './dom/operations.js'; import { HYDRATION_ERROR, HYDRATION_START, PassiveDelegatedEvents } from '../../constants.js'; -import { flush_sync, push, pop, current_component_context } from './runtime.js'; +import { flush_sync, push, pop, current_component_context, current_effect } from './runtime.js'; import { effect_root, branch } from './reactivity/effects.js'; import { hydrate_anchor, diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index 48c9b57f0e..ba4352d57b 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -3,7 +3,7 @@ import * as $ from "svelte/internal/client"; import TextInput from './Child.svelte'; var root_1 = $.template(`Something`, 1); -var root = $.template(` `, 5); +var root = $.template(` `, 1); export default function Bind_component_snippet($$anchor) { const snippet = ($$anchor) => { diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js index c1f5a2a309..bd24eca962 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js @@ -2,9 +2,5 @@ import "svelte/internal/disclose-version"; import * as $ from "svelte/internal/client"; export default function Bind_this($$anchor) { - var fragment = $.comment(true); - var node = $.first_child(fragment); - - $.bind_this(Foo(node, { $$legacy: true }), ($$value) => foo = $$value, () => foo); - $.append($$anchor, fragment); + $.bind_this(Foo($$anchor, { $$legacy: true }), ($$value) => foo = $$value, () => foo); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js index 98c5fd7ae7..badca8d4a0 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/server/index.svelte.js @@ -1,7 +1,5 @@ import * as $ from "svelte/internal/server"; export default function Bind_this($$payload) { - $$payload.out += ``; Foo($$payload, {}); - $$payload.out += ``; } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index 2cf005abdf..8fb6d075cd 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -9,10 +9,8 @@ export default function Function_prop_no_getter($$anchor) { } const plusOne = (num) => num + 1; - var fragment = $.comment(true); - var node = $.first_child(fragment); - Button(node, { + Button($$anchor, { onmousedown: () => $.set(count, $.get(count) + 1), onmouseup, onmouseenter: () => $.set(count, $.proxy(plusOne($.get(count)))), @@ -24,6 +22,4 @@ export default function Function_prop_no_getter($$anchor) { }, $$slots: { default: true } }); - - $.append($$anchor, fragment); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js index 974be7fcf6..8f90eaca2e 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/server/index.svelte.js @@ -9,8 +9,6 @@ export default function Function_prop_no_getter($$payload) { const plusOne = (num) => num + 1; - $$payload.out += ``; - Button($$payload, { onmousedown: () => count += 1, onmouseup, @@ -20,6 +18,4 @@ export default function Function_prop_no_getter($$payload) { }, $$slots: { default: true } }); - - $$payload.out += ``; } \ No newline at end of file