diff --git a/packages/svelte/src/internal/client/custom-element.js b/packages/svelte/src/internal/client/custom-element.js index 5374d6ba77..0bd64cdf50 100644 --- a/packages/svelte/src/internal/client/custom-element.js +++ b/packages/svelte/src/internal/client/custom-element.js @@ -1,5 +1,6 @@ import { createClassComponent } from '../../legacy/legacy-client.js'; -import { render_effect, destroy_signal } from './runtime.js'; +import { destroy_signal } from './runtime.js'; +import { render_effect } from './reactivity/effects.js'; import { open, close } from './render.js'; import { define_property } from './utils.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/await.js b/packages/svelte/src/internal/client/dom/blocks/await.js index 39946f63f2..0d7b8daf55 100644 --- a/packages/svelte/src/internal/client/dom/blocks/await.js +++ b/packages/svelte/src/internal/client/dom/blocks/await.js @@ -8,9 +8,9 @@ import { destroy_signal, execute_effect, flushSync, - push_destroy_fn, - render_effect + push_destroy_fn } from '../../runtime.js'; +import { render_effect } from '../../reactivity/effects.js'; import { trigger_transitions } from '../../transitions.js'; /** @returns {import('../../types.js').AwaitBlock} */ diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index d6367509b7..b068869c59 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -23,10 +23,10 @@ import { execute_effect, mutable_source, push_destroy_fn, - render_effect, set_signal_value, source } from '../../runtime.js'; +import { render_effect } from '../../reactivity/effects.js'; import { trigger_transitions } from '../../transitions.js'; import { is_array } from '../../utils.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/if.js b/packages/svelte/src/internal/client/dom/blocks/if.js index 75cfd7faed..cd03fe8c05 100644 --- a/packages/svelte/src/internal/client/dom/blocks/if.js +++ b/packages/svelte/src/internal/client/dom/blocks/if.js @@ -6,13 +6,8 @@ import { set_current_hydration_fragment } from '../../hydration.js'; import { remove } from '../../reconciler.js'; -import { - current_block, - destroy_signal, - execute_effect, - push_destroy_fn, - render_effect -} from '../../runtime.js'; +import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js'; +import { render_effect } from '../../reactivity/effects.js'; import { trigger_transitions } from '../../transitions.js'; /** @returns {import('../../types.js').IfBlock} */ diff --git a/packages/svelte/src/internal/client/dom/blocks/key.js b/packages/svelte/src/internal/client/dom/blocks/key.js index fda8828475..ed1541d327 100644 --- a/packages/svelte/src/internal/client/dom/blocks/key.js +++ b/packages/svelte/src/internal/client/dom/blocks/key.js @@ -7,9 +7,9 @@ import { destroy_signal, execute_effect, push_destroy_fn, - render_effect, safe_not_equal } from '../../runtime.js'; +import { render_effect } from '../../reactivity/effects.js'; import { trigger_transitions } from '../../transitions.js'; /** @returns {import('../../types.js').KeyBlock} */ diff --git a/packages/svelte/src/internal/client/proxy.js b/packages/svelte/src/internal/client/proxy.js index 8f26e67806..9a8cf2cb63 100644 --- a/packages/svelte/src/internal/client/proxy.js +++ b/packages/svelte/src/internal/client/proxy.js @@ -1,6 +1,5 @@ import { DEV } from 'esm-env'; import { - effect_active, get, set, update, @@ -11,6 +10,7 @@ import { batch_inspect, current_component_context } from './runtime.js'; +import { effect_active } from './reactivity/effects.js'; import { array_prototype, define_property, diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js new file mode 100644 index 0000000000..955e91b215 --- /dev/null +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -0,0 +1,180 @@ +import { DEV } from 'esm-env'; +import { + DIRTY, + EFFECT, + MANAGED, + PRE_EFFECT, + RENDER_EFFECT, + create_computation_signal, + current_block, + current_component_context, + current_effect, + destroy_signal, + flush_local_render_effects, + push_reference, + schedule_effect +} from '../runtime.js'; + +/** + * @param {import('../types.js').EffectType} type + * @param {(() => void | (() => void)) | ((b: import('../types.js').Block) => void | (() => void))} init + * @param {boolean} sync + * @param {null | import('../types.js').Block} block + * @param {boolean} schedule + * @returns {import('../types.js').EffectSignal} + */ +function internal_create_effect(type, init, sync, block, schedule) { + const signal = create_computation_signal(type | DIRTY, null, block); + signal.i = init; + signal.x = current_component_context; + if (current_effect !== null) { + signal.l = current_effect.l + 1; + if ((type & MANAGED) === 0) { + push_reference(current_effect, signal); + } + } + if (schedule) { + schedule_effect(signal, sync); + } + return signal; +} + +/** + * @returns {boolean} + */ +export function effect_active() { + return current_effect ? (current_effect.f & MANAGED) === 0 : false; +} + +/** + * @param {() => void | (() => void)} init + * @returns {import('../types.js').EffectSignal} + */ +export function user_effect(init) { + if (current_effect === null) { + throw new Error( + 'ERR_SVELTE_ORPHAN_EFFECT' + + (DEV ? ': The Svelte $effect rune can only be used during component initialisation.' : '') + ); + } + const apply_component_effect_heuristics = + current_effect.f & RENDER_EFFECT && + current_component_context !== null && + !current_component_context.m; + const effect = internal_create_effect( + EFFECT, + init, + false, + current_block, + !apply_component_effect_heuristics + ); + if (apply_component_effect_heuristics) { + const context = /** @type {import('../types.js').ComponentContext} */ ( + current_component_context + ); + (context.e ??= []).push(effect); + } + return effect; +} + +/** + * @param {() => void | (() => void)} init + * @returns {() => void} + */ +export function user_root_effect(init) { + const effect = managed_render_effect(init); + return () => { + destroy_signal(effect); + }; +} + +/** + * @param {() => void | (() => void)} init + * @returns {import('../types.js').EffectSignal} + */ +export function effect(init) { + return internal_create_effect(EFFECT, init, false, current_block, true); +} + +/** + * @param {() => void | (() => void)} init + * @returns {import('../types.js').EffectSignal} + */ +export function managed_effect(init) { + return internal_create_effect(EFFECT | MANAGED, init, false, current_block, true); +} + +/** + * @param {() => void | (() => void)} init + * @param {boolean} sync + * @returns {import('../types.js').EffectSignal} + */ +export function managed_pre_effect(init, sync) { + return internal_create_effect(PRE_EFFECT | MANAGED, init, sync, current_block, true); +} + +/** + * @param {() => void | (() => void)} init + * @returns {import('../types.js').EffectSignal} + */ +export function pre_effect(init) { + if (current_effect === null) { + throw new Error( + 'ERR_SVELTE_ORPHAN_EFFECT' + + (DEV + ? ': The Svelte $effect.pre rune can only be used during component initialisation.' + : '') + ); + } + const sync = current_effect !== null && (current_effect.f & RENDER_EFFECT) !== 0; + return internal_create_effect( + PRE_EFFECT, + () => { + const val = init(); + flush_local_render_effects(); + return val; + }, + sync, + current_block, + true + ); +} + +/** + * 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)} init + * @returns {import('../types.js').EffectSignal} + */ +export function invalidate_effect(init) { + return internal_create_effect(PRE_EFFECT, init, true, current_block, true); +} + +/** + * @template {import('../types.js').Block} B + * @param {(block: B) => void | (() => void)} init + * @param {any} block + * @param {any} managed + * @param {any} sync + * @returns {import('../types.js').EffectSignal} + */ +export function render_effect(init, block = current_block, managed = false, sync = true) { + let flags = RENDER_EFFECT; + if (managed) { + flags |= MANAGED; + } + return internal_create_effect(flags, /** @type {any} */ (init), sync, block, true); +} + +/** + * @template {import('../types.js').Block} B + * @param {(block: B) => void | (() => void)} init + * @param {any} block + * @param {any} sync + * @returns {import('../types.js').EffectSignal} + */ +export function managed_render_effect(init, block = current_block, sync = true) { + const flags = RENDER_EFFECT | MANAGED; + return internal_create_effect(flags, /** @type {any} */ (init), sync, block, true); +} diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 8a76d2d98d..cc35acc468 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -31,21 +31,19 @@ import { remove } from './reconciler.js'; import { - render_effect, destroy_signal, is_signal, push_destroy_fn, execute_effect, untrack, - effect, flush_sync, current_block, - managed_effect, push, current_component_context, pop, deep_read } from './runtime.js'; +import { render_effect, effect, managed_effect } from './reactivity/effects.js'; import { current_hydration_fragment, get_hydration_fragment, diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 47547636e1..87bc7f4291 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -19,13 +19,14 @@ import { } from '../../constants.js'; import { STATE_SYMBOL, unstate } from './proxy.js'; import { EACH_BLOCK, IF_BLOCK } from './block.js'; +import { pre_effect, user_effect } from './reactivity/effects.js'; export const SOURCE = 1; export const DERIVED = 1 << 1; export const EFFECT = 1 << 2; export const PRE_EFFECT = 1 << 3; export const RENDER_EFFECT = 1 << 4; -const MANAGED = 1 << 6; +export const MANAGED = 1 << 6; const UNOWNED = 1 << 7; export const CLEAN = 1 << 8; export const DIRTY = 1 << 9; @@ -197,7 +198,7 @@ function create_source_signal(flags, value) { * @param {import('./types.js').Block | null} block * @returns {import('./types.js').ComputationSignal | import('./types.js').ComputationSignal & import('./types.js').SourceSignalDebug} */ -function create_computation_signal(flags, value, block) { +export function create_computation_signal(flags, value, block) { if (DEV) { return { // block @@ -262,7 +263,7 @@ function create_computation_signal(flags, value, block) { * @param {import('./types.js').ComputationSignal} ref_signal * @returns {void} */ -function push_reference(target_signal, ref_signal) { +export function push_reference(target_signal, ref_signal) { const references = target_signal.r; if (references === null) { target_signal.r = [ref_signal]; @@ -1374,170 +1375,6 @@ export function untrack(fn) { } } -/** - * @param {import('./types.js').EffectType} type - * @param {(() => void | (() => void)) | ((b: import('./types.js').Block) => void | (() => void))} init - * @param {boolean} sync - * @param {null | import('./types.js').Block} block - * @param {boolean} schedule - * @returns {import('./types.js').EffectSignal} - */ -function internal_create_effect(type, init, sync, block, schedule) { - const signal = create_computation_signal(type | DIRTY, null, block); - signal.i = init; - signal.x = current_component_context; - if (current_effect !== null) { - signal.l = current_effect.l + 1; - if ((type & MANAGED) === 0) { - push_reference(current_effect, signal); - } - } - if (schedule) { - schedule_effect(signal, sync); - } - return signal; -} - -/** - * @returns {boolean} - */ -export function effect_active() { - return current_effect ? (current_effect.f & MANAGED) === 0 : false; -} - -/** - * @param {() => void | (() => void)} init - * @returns {import('./types.js').EffectSignal} - */ -export function user_effect(init) { - if (current_effect === null) { - throw new Error( - 'ERR_SVELTE_ORPHAN_EFFECT' + - (DEV ? ': The Svelte $effect rune can only be used during component initialisation.' : '') - ); - } - const apply_component_effect_heuristics = - current_effect.f & RENDER_EFFECT && - current_component_context !== null && - !current_component_context.m; - const effect = internal_create_effect( - EFFECT, - init, - false, - current_block, - !apply_component_effect_heuristics - ); - if (apply_component_effect_heuristics) { - const context = /** @type {import('./types.js').ComponentContext} */ ( - current_component_context - ); - (context.e ??= []).push(effect); - } - return effect; -} - -/** - * @param {() => void | (() => void)} init - * @returns {() => void} - */ -export function user_root_effect(init) { - const effect = managed_render_effect(init); - return () => { - destroy_signal(effect); - }; -} - -/** - * @param {() => void | (() => void)} init - * @returns {import('./types.js').EffectSignal} - */ -export function effect(init) { - return internal_create_effect(EFFECT, init, false, current_block, true); -} - -/** - * @param {() => void | (() => void)} init - * @returns {import('./types.js').EffectSignal} - */ -export function managed_effect(init) { - return internal_create_effect(EFFECT | MANAGED, init, false, current_block, true); -} - -/** - * @param {() => void | (() => void)} init - * @param {boolean} sync - * @returns {import('./types.js').EffectSignal} - */ -export function managed_pre_effect(init, sync) { - return internal_create_effect(PRE_EFFECT | MANAGED, init, sync, current_block, true); -} - -/** - * @param {() => void | (() => void)} init - * @returns {import('./types.js').EffectSignal} - */ -export function pre_effect(init) { - if (current_effect === null) { - throw new Error( - 'ERR_SVELTE_ORPHAN_EFFECT' + - (DEV - ? ': The Svelte $effect.pre rune can only be used during component initialisation.' - : '') - ); - } - const sync = current_effect !== null && (current_effect.f & RENDER_EFFECT) !== 0; - return internal_create_effect( - PRE_EFFECT, - () => { - const val = init(); - flush_local_render_effects(); - return val; - }, - sync, - current_block, - true - ); -} - -/** - * 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)} init - * @returns {import('./types.js').EffectSignal} - */ -export function invalidate_effect(init) { - return internal_create_effect(PRE_EFFECT, init, true, current_block, true); -} - -/** - * @template {import('./types.js').Block} B - * @param {(block: B) => void | (() => void)} init - * @param {any} block - * @param {any} managed - * @param {any} sync - * @returns {import('./types.js').EffectSignal} - */ -export function render_effect(init, block = current_block, managed = false, sync = true) { - let flags = RENDER_EFFECT; - if (managed) { - flags |= MANAGED; - } - return internal_create_effect(flags, /** @type {any} */ (init), sync, block, true); -} - -/** - * @template {import('./types.js').Block} B - * @param {(block: B) => void | (() => void)} init - * @param {any} block - * @param {any} sync - * @returns {import('./types.js').EffectSignal} - */ -export function managed_render_effect(init, block = current_block, sync = true) { - const flags = RENDER_EFFECT | MANAGED; - return internal_create_effect(flags, /** @type {any} */ (init), sync, block, true); -} - /** * @template V * @param {import('./types.js').ComputationSignal} signal diff --git a/packages/svelte/src/internal/client/transitions.js b/packages/svelte/src/internal/client/transitions.js index 01350350e9..7cb5f3a36c 100644 --- a/packages/svelte/src/internal/client/transitions.js +++ b/packages/svelte/src/internal/client/transitions.js @@ -11,14 +11,12 @@ import { } from './block.js'; import { destroy_each_item_block, get_first_element } from './dom/blocks/each.js'; import { append_child, empty } from './operations.js'; +import { effect, managed_effect, managed_pre_effect } from './reactivity/effects.js'; import { current_block, current_effect, destroy_signal, - effect, execute_effect, - managed_effect, - managed_pre_effect, mark_subtree_inert, schedule_raf_task, untrack diff --git a/packages/svelte/src/internal/index.js b/packages/svelte/src/internal/index.js index 72026e9389..aa38e22d35 100644 --- a/packages/svelte/src/internal/index.js +++ b/packages/svelte/src/internal/index.js @@ -9,10 +9,6 @@ export { derived, derived_safe_equal, prop, - user_effect, - render_effect, - pre_effect, - invalidate_effect, flushSync, bubble_event, safe_equal, @@ -33,8 +29,6 @@ export { pop, push, reactive_import, - effect_active, - user_root_effect, inspect, unwrap, freeze, @@ -46,6 +40,7 @@ export { await_block as await } from './client/dom/blocks/await.js'; export { if_block as if } from './client/dom/blocks/if.js'; export { key_block as key } from './client/dom/blocks/key.js'; export * from './client/dom/blocks/each.js'; +export * from './client/reactivity/effects.js'; export * from './client/render.js'; export * from './client/validate.js'; export { raf } from './client/timing.js'; diff --git a/packages/svelte/src/main/main-client.js b/packages/svelte/src/main/main-client.js index 1b2ba77725..690f6946a2 100644 --- a/packages/svelte/src/main/main-client.js +++ b/packages/svelte/src/main/main-client.js @@ -1,10 +1,10 @@ import { current_component_context, get_or_init_context_map, - untrack, - user_effect + untrack } from '../internal/client/runtime.js'; import { is_array } from '../internal/client/utils.js'; +import { user_effect } from '../internal/index.js'; /** * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 9cecf3d5ea..2713ae9ba1 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -1,5 +1,6 @@ import { describe, assert, it } from 'vitest'; import * as $ from '../../src/internal/client/runtime'; +import { effect, render_effect, user_effect } from '../../src/internal/client/reactivity/effects'; import type { ComputationSignal } from '../../src/internal/client/types'; /** @@ -13,7 +14,7 @@ function run_test(runes: boolean, fn: (runes: boolean) => () => void) { $.push({}, runes); // Create a render context so that effect validations etc don't fail let execute: any; - const signal = $.render_effect( + const signal = render_effect( () => { execute = fn(runes); }, @@ -38,7 +39,7 @@ describe('signals', () => { let count = $.source(0); let double = $.derived(() => $.get(count) * 2); - $.effect(() => { + effect(() => { log.push(`${$.get(count)}:${$.get(double)}`); }); @@ -56,10 +57,10 @@ describe('signals', () => { let count = $.source(0); let double = $.derived(() => $.get(count) * 2); - $.effect(() => { + effect(() => { log.push(`A:${$.get(count)}:${$.get(double)}`); }); - $.effect(() => { + effect(() => { log.push(`B:${$.get(double)}`); }); @@ -77,10 +78,10 @@ describe('signals', () => { let count = $.source(0); let double = $.derived(() => $.get(count) * 2); - $.effect(() => { + effect(() => { log.push(`A:${$.get(double)}`); }); - $.effect(() => { + effect(() => { log.push(`B:${$.get(count)}:${$.get(double)}`); }); @@ -98,7 +99,7 @@ describe('signals', () => { let count = $.source(0); let double = $.derived(() => $.get(count) * 2); - $.effect(() => { + effect(() => { log.push($.get(double)); }); @@ -117,7 +118,7 @@ describe('signals', () => { let double = $.derived(() => $.get(count) * 2); let quadruple = $.derived(() => $.get(double) * 2); - $.effect(() => { + effect(() => { log.push($.get(quadruple)); }); @@ -143,13 +144,13 @@ describe('signals', () => { const E = $.derived(() => hard($.get(C) + $.get(A) + $.get(D)[0]!, 'E')); const F = $.derived(() => hard($.get(D)[0]! && $.get(B), 'F')); const G = $.derived(() => $.get(C) + ($.get(C) || $.get(E) % 2) + $.get(D)[0]! + $.get(F)); - $.effect(() => { + effect(() => { res.push(hard($.get(G), 'H')); }); - $.effect(() => { + effect(() => { res.push($.get(G)); }); - $.effect(() => { + effect(() => { res.push(hard($.get(F), 'J')); }); @@ -182,7 +183,7 @@ describe('signals', () => { return $.get(x); }; const derivedCount = $.derived(() => read().count); - $.user_effect(() => { + user_effect(() => { log.push($.get(derivedCount)); }); @@ -244,7 +245,7 @@ describe('signals', () => { test('effect with derived using unowned derived every time', () => { const log: Array = []; - const effect = $.user_effect(() => { + const effect = user_effect(() => { log.push($.get(calc)); }); @@ -270,11 +271,11 @@ describe('signals', () => { test('two effects with an unowned derived that has no depedencies', () => { const log: Array> = []; - $.render_effect(() => { + render_effect(() => { log.push($.get(no_deps)); }); - $.render_effect(() => { + render_effect(() => { log.push($.get(no_deps)); }); @@ -292,11 +293,11 @@ describe('signals', () => { test('two effects with an unowned derived that has some depedencies', () => { const log: Array> = []; - $.render_effect(() => { + render_effect(() => { log.push($.get(some_deps)); }); - $.render_effect(() => { + render_effect(() => { log.push($.get(some_deps)); }); @@ -310,7 +311,7 @@ describe('signals', () => { if (!runes) return () => {}; const value = $.source({ count: 0 }); - $.user_effect(() => { + user_effect(() => { $.set(value, { count: 0 }); $.get(value); });