diff --git a/.changeset/eleven-beers-yell.md b/.changeset/eleven-beers-yell.md new file mode 100644 index 0000000000..9f38d30e9c --- /dev/null +++ b/.changeset/eleven-beers-yell.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +feat: use implicit return for each block keys 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 a89ae00f70..b524391380 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 @@ -371,7 +371,7 @@ export const javascript_visitors_runes = { const func = context.visit(node.expression.arguments[0]); return { ...node, - expression: b.call('$.pre_effect', /** @type {import('estree').Expression} */ (func)) + expression: b.call('$.user_pre_effect', /** @type {import('estree').Expression} */ (func)) }; } } 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 4de46a65b7..edbfe19bde 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 @@ -231,7 +231,7 @@ function setup_select_synchronization(value_binding, context) { context.state.init.push( b.stmt( b.call( - '$.invalidate_effect', + '$.pre_effect', b.thunk( b.block([ b.stmt( @@ -747,7 +747,7 @@ function serialize_inline_component(node, component_name, context) { binding_initializers.push( b.stmt( b.call( - b.id('$.pre_effect'), + b.id('$.user_pre_effect'), b.thunk(b.call(b.id('$.add_owner'), expression, b.id(component_name))) ) ) @@ -2329,11 +2329,13 @@ export const template_visitors = { const key_function = node.key ? b.arrow( [node.context.type === 'Identifier' ? node.context : b.id('$$item'), index], - b.block( - declarations.concat( - b.return(/** @type {import('estree').Expression} */ (context.visit(node.key))) - ) - ) + declarations.length > 0 + ? b.block( + declarations.concat( + b.return(/** @type {import('estree').Expression} */ (context.visit(node.key))) + ) + ) + : /** @type {import('estree').Expression} */ (context.visit(node.key)) ) : b.literal(null); @@ -2359,16 +2361,16 @@ export const template_visitors = { args.push( context.state.node, - each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection), b.literal(each_type), + each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection), key_function, b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children))) ); } else { args.push( context.state.node, - each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection), b.literal(each_type), + each_node_meta.array_name ? each_node_meta.array_name : b.thunk(collection), b.arrow([b.id('$$anchor'), item, index], b.block(declarations.concat(children))) ); } diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index d74f6adad5..02ec4b94e7 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -46,15 +46,15 @@ export function set_current_each_item(item) { /** * @template V * @param {Element | Comment} anchor The next sibling node, or the parent node if this is a 'controlled' block - * @param {() => V[]} get_collection * @param {number} flags + * @param {() => V[]} get_collection * @param {null | ((item: V) => string)} get_key * @param {(anchor: null, item: V, index: import('#client').MaybeSource) => void} render_fn * @param {null | ((anchor: Node | null) => void)} fallback_fn * @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn * @returns {void} */ -function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, reconcile_fn) { +function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_fn) { /** @type {import('#client').EachState} */ var state = { flags, items: [] }; @@ -193,28 +193,28 @@ function each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, re /** * @template V * @param {Element | Comment} anchor - * @param {() => V[]} get_collection * @param {number} flags + * @param {() => V[]} get_collection * @param {null | ((item: V) => string)} get_key * @param {(anchor: null, item: V, index: import('#client').MaybeSource) => void} render_fn * @param {null | ((anchor: Node | null) => void)} [fallback_fn] * @returns {void} */ -export function each_keyed(anchor, get_collection, flags, get_key, render_fn, fallback_fn = null) { - each(anchor, get_collection, flags, get_key, render_fn, fallback_fn, reconcile_tracked_array); +export function each_keyed(anchor, flags, get_collection, get_key, render_fn, fallback_fn = null) { + each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, reconcile_tracked_array); } /** * @template V * @param {Element | Comment} anchor - * @param {() => V[]} get_collection * @param {number} flags + * @param {() => V[]} get_collection * @param {(anchor: null, item: V, index: import('#client').MaybeSource) => void} render_fn * @param {null | ((anchor: Node | null) => void)} [fallback_fn] * @returns {void} */ -export function each_indexed(anchor, get_collection, flags, render_fn, fallback_fn = null) { - each(anchor, get_collection, flags, null, render_fn, fallback_fn, reconcile_indexed_array); +export function each_indexed(anchor, flags, get_collection, render_fn, fallback_fn = null) { + each(anchor, flags, get_collection, null, render_fn, fallback_fn, reconcile_indexed_array); } /** diff --git a/packages/svelte/src/internal/client/dom/legacy/lifecycle.js b/packages/svelte/src/internal/client/dom/legacy/lifecycle.js index 425f65398a..e186881197 100644 --- a/packages/svelte/src/internal/client/dom/legacy/lifecycle.js +++ b/packages/svelte/src/internal/client/dom/legacy/lifecycle.js @@ -1,5 +1,5 @@ import { run } from '../../../common.js'; -import { pre_effect, user_effect } from '../../reactivity/effects.js'; +import { user_pre_effect, user_effect } from '../../reactivity/effects.js'; import { current_component_context, deep_read_state, @@ -19,7 +19,7 @@ export function init() { // beforeUpdate if (callbacks.b.length) { - pre_effect(() => { + user_pre_effect(() => { observe_all(context); callbacks.b.forEach(run); // beforeUpdate might change state that affects rendering, ensure the render effects following from it diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 2e631083d4..b3b76d8c6a 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -7,8 +7,10 @@ import { destroy_children, execute_effect, get, + is_flushing_effect, remove_reactions, schedule_effect, + set_is_flushing_effect, set_signal_status, untrack } from '../runtime.js'; @@ -20,7 +22,8 @@ import { PRE_EFFECT, DESTROYED, INERT, - IS_ELSEIF + IS_ELSEIF, + EFFECT_RAN } from '../constants.js'; import { set } from './sources.js'; import { noop } from '../../common.js'; @@ -35,7 +38,7 @@ import { remove } from '../dom/reconciler.js'; */ function create_effect(type, fn, sync, init = true) { /** @type {import('#client').Effect} */ - const signal = { + const effect = { parent: current_effect, dom: null, deps: null, @@ -51,22 +54,34 @@ function create_effect(type, fn, sync, init = true) { }; if (current_effect !== null) { - signal.l = current_effect.l + 1; + effect.l = current_effect.l + 1; } if (current_reaction !== null) { if (current_reaction.effects === null) { - current_reaction.effects = [signal]; + current_reaction.effects = [effect]; } else { - current_reaction.effects.push(signal); + current_reaction.effects.push(effect); } } if (init) { - schedule_effect(signal, sync); + if (sync) { + const previously_flushing_effect = is_flushing_effect; + + try { + set_is_flushing_effect(true); + execute_effect(effect); + effect.f |= EFFECT_RAN; + } finally { + set_is_flushing_effect(previously_flushing_effect); + } + } else { + schedule_effect(effect); + } } - return signal; + return effect; } /** @@ -108,6 +123,24 @@ export function user_effect(fn) { return effect; } +/** + * Internal representation of `$effect.pre(...)` + * @param {() => void | (() => void)} fn + * @returns {import('#client').Effect} + */ +export function user_pre_effect(fn) { + if (current_effect === null) { + throw new Error( + 'ERR_SVELTE_ORPHAN_EFFECT' + + (DEV + ? ': The Svelte $effect.pre rune can only be used during component initialisation.' + : '') + ); + } + + return pre_effect(fn); +} + /** * Internal representation of `$effect.root(...)` * @param {() => void | (() => void)} fn @@ -128,24 +161,6 @@ export function effect(fn) { return create_effect(EFFECT, fn, false); } -/** - * Internal representation of `$effect.pre(...)` - * @param {() => void | (() => void)} fn - * @returns {import('#client').Effect} - */ -export function pre_effect(fn) { - if (current_effect === null) { - throw new Error( - 'ERR_SVELTE_ORPHAN_EFFECT' + - (DEV - ? ': The Svelte $effect.pre rune can only be used during component initialisation.' - : '') - ); - } - - return create_effect(PRE_EFFECT, fn, true); -} - /** * Internal representation of `$: ..` * @param {() => any} deps @@ -157,19 +172,15 @@ export function legacy_pre_effect(deps, fn) { current_component_context ); const token = {}; - return create_effect( - PRE_EFFECT, - () => { - deps(); - if (component_context.l1.includes(token)) { - return; - } - component_context.l1.push(token); - set(component_context.l2, true); - return untrack(fn); - }, - true - ); + return pre_effect(() => { + deps(); + if (component_context.l1.includes(token)) { + return; + } + component_context.l1.push(token); + set(component_context.l2, true); + return untrack(fn); + }); } export function legacy_pre_effect_reset() { @@ -186,13 +197,10 @@ export function legacy_pre_effect_reset() { } /** - * This effect is used to ensure binding are kept in sync. We use a pre effect to ensure we run before the - * bindings which are in later effects. However, we don't use a pre_effect directly as we don't want to flush anything. - * * @param {() => void | (() => void)} fn * @returns {import('#client').Effect} */ -export function invalidate_effect(fn) { +export function pre_effect(fn) { return create_effect(PRE_EFFECT, fn, true); } diff --git a/packages/svelte/src/internal/client/reactivity/sources.js b/packages/svelte/src/internal/client/reactivity/sources.js index 44eb2b1ad8..64113bfff4 100644 --- a/packages/svelte/src/internal/client/reactivity/sources.js +++ b/packages/svelte/src/internal/client/reactivity/sources.js @@ -135,7 +135,7 @@ export function set(signal, value) { ) { if (current_dependencies !== null && current_dependencies.includes(signal)) { set_signal_status(current_effect, DIRTY); - schedule_effect(current_effect, false); + schedule_effect(current_effect); } else { if (current_untracked_writes === null) { set_current_untracked_writes([signal]); diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 2febac4861..c5b336ca2d 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -8,7 +8,7 @@ import { object_prototype } from './utils.js'; import { unstate } from './proxy.js'; -import { destroy_effect, pre_effect } from './reactivity/effects.js'; +import { destroy_effect, user_pre_effect } from './reactivity/effects.js'; import { EFFECT, PRE_EFFECT, @@ -36,7 +36,13 @@ const FLUSH_SYNC = 1; let current_scheduler_mode = FLUSH_MICROTASK; // Used for handling scheduling let is_micro_task_queued = false; -let is_flushing_effect = false; +export let is_flushing_effect = false; + +/** @param {boolean} value */ +export function set_is_flushing_effect(value) { + is_flushing_effect = value; +} + // Used for $inspect export let is_batching_effect = false; let is_inspecting_signal = false; @@ -210,9 +216,6 @@ export function check_dirtiness(reaction) { * @returns {V} */ export function execute_reaction_fn(signal) { - const fn = signal.fn; - const flags = signal.f; - const previous_dependencies = current_dependencies; const previous_dependencies_index = current_dependencies_index; const previous_untracked_writes = current_untracked_writes; @@ -224,11 +227,11 @@ export function execute_reaction_fn(signal) { current_dependencies_index = 0; current_untracked_writes = null; current_reaction = signal; - current_skip_reaction = !is_flushing_effect && (flags & UNOWNED) !== 0; + current_skip_reaction = !is_flushing_effect && (signal.f & UNOWNED) !== 0; current_untracking = false; try { - let res = fn(); + let res = signal.fn(); let dependencies = /** @type {import('./types.js').Value[]} **/ (signal.deps); if (current_dependencies !== null) { let i; @@ -373,33 +376,35 @@ export function destroy_children(signal) { } /** - * @param {import('./types.js').Effect} signal + * @param {import('./types.js').Effect} effect * @returns {void} */ -export function execute_effect(signal) { - if ((signal.f & DESTROYED) !== 0) { +export function execute_effect(effect) { + if ((effect.f & DESTROYED) !== 0) { return; } - const previous_effect = current_effect; - const previous_component_context = current_component_context; + set_signal_status(effect, CLEAN); - const component_context = signal.ctx; + var component_context = effect.ctx; - current_effect = signal; + var previous_effect = current_effect; + var previous_component_context = current_component_context; + + current_effect = effect; current_component_context = component_context; try { - destroy_children(signal); - signal.teardown?.(); - const teardown = execute_reaction_fn(signal); - signal.teardown = typeof teardown === 'function' ? teardown : null; + destroy_children(effect); + effect.teardown?.(); + var teardown = execute_reaction_fn(effect); + effect.teardown = typeof teardown === 'function' ? teardown : null; } finally { current_effect = previous_effect; current_component_context = previous_component_context; } - if ((signal.f & PRE_EFFECT) !== 0 && current_queued_pre_and_render_effects.length > 0) { + if ((effect.f & PRE_EFFECT) !== 0 && current_queued_pre_and_render_effects.length > 0) { flush_local_pre_effects(component_context); } } @@ -436,7 +441,6 @@ function flush_queued_effects(effects) { if ((signal.f & (DESTROYED | INERT)) === 0) { if (check_dirtiness(signal)) { - set_signal_status(signal, CLEAN); execute_effect(signal); } } @@ -466,85 +470,73 @@ function process_microtask() { /** * @param {import('./types.js').Effect} signal - * @param {boolean} sync * @returns {void} */ -export function schedule_effect(signal, sync) { +export function schedule_effect(signal) { const flags = signal.f; - if (sync) { - const previously_flushing_effect = is_flushing_effect; - try { - is_flushing_effect = true; - execute_effect(signal); - set_signal_status(signal, CLEAN); - } finally { - is_flushing_effect = previously_flushing_effect; + + if (current_scheduler_mode === FLUSH_MICROTASK) { + if (!is_micro_task_queued) { + is_micro_task_queued = true; + queueMicrotask(process_microtask); } - } else { - if (current_scheduler_mode === FLUSH_MICROTASK) { - if (!is_micro_task_queued) { - is_micro_task_queued = true; - queueMicrotask(process_microtask); - } + } + + if ((flags & EFFECT) !== 0) { + current_queued_effects.push(signal); + // Prevent any nested user effects from potentially triggering + // 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) { + mark_subtree_children_inert(signal, true); } - if ((flags & EFFECT) !== 0) { - current_queued_effects.push(signal); - // Prevent any nested user effects from potentially triggering - // 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) { - mark_subtree_children_inert(signal, true); - } - } else { - // We need to ensure we insert the signal in the right topological order. In other words, - // we need to evaluate where to insert the signal based off its level and whether or not it's - // a pre-effect and within the same block. By checking the signals in the queue in reverse order - // we can find the right place quickly. TODO: maybe opt to use a linked list rather than an array - // for these operations. - const length = current_queued_pre_and_render_effects.length; - let should_append = length === 0; - - if (!should_append) { - const target_level = signal.l; - const is_pre_effect = (flags & PRE_EFFECT) !== 0; - let target_signal; - let target_signal_level; - let is_target_pre_effect; - let i = length; - while (true) { - target_signal = current_queued_pre_and_render_effects[--i]; - target_signal_level = target_signal.l; - if (target_signal_level <= target_level) { - if (i + 1 === length) { - should_append = true; - } else { - is_target_pre_effect = (target_signal.f & PRE_EFFECT) !== 0; - if ( - target_signal_level < target_level || - target_signal !== signal || - (is_target_pre_effect && !is_pre_effect) - ) { - i++; - } - current_queued_pre_and_render_effects.splice(i, 0, signal); + } else { + // We need to ensure we insert the signal in the right topological order. In other words, + // we need to evaluate where to insert the signal based off its level and whether or not it's + // a pre-effect and within the same block. By checking the signals in the queue in reverse order + // we can find the right place quickly. TODO: maybe opt to use a linked list rather than an array + // for these operations. + const length = current_queued_pre_and_render_effects.length; + let should_append = length === 0; + + if (!should_append) { + const target_level = signal.l; + const is_pre_effect = (flags & PRE_EFFECT) !== 0; + let target_signal; + let target_signal_level; + let is_target_pre_effect; + let i = length; + while (true) { + target_signal = current_queued_pre_and_render_effects[--i]; + target_signal_level = target_signal.l; + if (target_signal_level <= target_level) { + if (i + 1 === length) { + should_append = true; + } else { + is_target_pre_effect = (target_signal.f & PRE_EFFECT) !== 0; + if ( + target_signal_level < target_level || + target_signal !== signal || + (is_target_pre_effect && !is_pre_effect) + ) { + i++; } - break; - } - if (i === 0) { - current_queued_pre_and_render_effects.unshift(signal); - break; + current_queued_pre_and_render_effects.splice(i, 0, signal); } + break; + } + if (i === 0) { + current_queued_pre_and_render_effects.unshift(signal); + break; } } + } - if (should_append) { - current_queued_pre_and_render_effects.push(signal); - } + if (should_append) { + current_queued_pre_and_render_effects.push(signal); } } - - signal.f |= EFFECT_RAN; } /** @@ -696,7 +688,7 @@ export function get(signal) { current_untracked_writes.includes(signal) ) { set_signal_status(current_effect, DIRTY); - schedule_effect(current_effect, false); + schedule_effect(current_effect); } } @@ -772,7 +764,7 @@ export function mark_subtree_inert(signal, inert) { if (is_already_inert !== inert) { signal.f ^= INERT; if (!inert && (flags & CLEAN) === 0) { - schedule_effect(signal, false); + schedule_effect(signal); } } @@ -819,7 +811,7 @@ export function mark_reactions(signal, to_status, force_schedule) { force_schedule ); } else { - schedule_effect(/** @type {import('#client').Effect} */ (reaction), false); + schedule_effect(/** @type {import('#client').Effect} */ (reaction)); } } } @@ -1075,7 +1067,7 @@ export function pop(component) { if (effects !== null) { context_stack_item.e = null; for (let i = 0; i < effects.length; i++) { - schedule_effect(effects[i], false); + schedule_effect(effects[i]); } } current_component_context = context_stack_item.p; @@ -1201,7 +1193,7 @@ let warned_inspect_changed = false; export function inspect(get_value, inspect = console.log) { let initial = true; - pre_effect(() => { + user_pre_effect(() => { const fn = () => { const value = untrack(() => get_value().map((v) => deep_unstate(v))); if (value.length === 2 && typeof value[1] === 'function' && !warned_inspect_changed) { diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js index 06b05e4abf..7e1177d2b7 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js @@ -10,7 +10,7 @@ export default function Each_string_template($$anchor, $$props) { var fragment = $.comment($$anchor); var node = $.first_child(fragment); - $.each_indexed(node, () => ['foo', 'bar', 'baz'], 1, ($$anchor, thing, $$index) => { + $.each_indexed(node, 1, () => ['foo', 'bar', 'baz'], ($$anchor, thing, $$index) => { var text = $.space_frag($$anchor); $.render_effect(() => $.set_text(text, `${$.stringify($.unwrap(thing))}, `));