chore: refactor reactivity code (#10760)

* split destroy_signal into destroy_effect and destroy_derived

* remove unused code

* remove derived.y

* remove unused code

* lint

* use alias

* simplify

* remove unused code

* remove unused code

* rename value.c to value.reactions, remove from effect type

* rename consumer to reaction, for greater consistency

* remove push_destroy_fn, we don't need it

* more clarity

* this is just incorrect

* align with 10594

* rename signal.i to signal.fn

* rename signal.d to signal.deps

* remove some unnecessary ceremony

* remove unnecessary code

* believe it or not this is faster. it also looks cooler

* tidy up

* remove derived.x

* tighten up mark_subtree_insert

* remove derived.b

* tidy up

* tidy up

* effect.x -> effect.ctx

* signal.b -> signal.block

* tighten up

* tidy up

* separate reaction.r into reaction.effects and reaction.deriveds

* make Derived and Effect inherit from Reaction, rather than Reaction being a union of Derived and Effect

* remove unused stuff

* tidy up

* rename effect.v to effect.teardown

* rename effect.y to effect.ondestroy

* tidy up

* inline doc

* rename

* simplify

* tidy up

* tweak

* remove unnecessary if check

* fix effect teardown bug

* test

* stfu eslint

* remove unused is_runes call

* remove unused argument

* this code doesnt do anything

* typos

* unused import

* source needs no flags

* tidy up

* tidy up

* move some code

* tidy up

* set current signal status, not dependency status

* simplify

* remove unused code

* tidy up

* rename — is_signal_dirty sounds like a pure function but it isnt

* rename value.w to value.version

* rename

* simplify

* MaybeSignal -> MaybeSource

* unused argument

* use default parameters

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/10769/head
Rich Harris 4 months ago committed by GitHub
parent 3196077b5d
commit 278155968e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,4 +1,3 @@
export const SOURCE = 1;
export const DERIVED = 1 << 1;
export const EFFECT = 1 << 2;
export const PRE_EFFECT = 1 << 3;

@ -1,6 +1,5 @@
import { createClassComponent } from '../../legacy/legacy-client.js';
import { destroy_signal } from './runtime.js';
import { render_effect } from './reactivity/effects.js';
import { destroy_effect, render_effect } from './reactivity/effects.js';
import { open, close } from './render.js';
import { define_property } from './utils.js';
@ -199,7 +198,7 @@ if (typeof HTMLElement === 'function') {
Promise.resolve().then(() => {
if (!this.$$cn) {
this.$$c.$destroy();
destroy_signal(this.$$me);
destroy_effect(this.$$me);
this.$$c = undefined;
}
});

@ -1,14 +1,8 @@
import { is_promise } from '../../../common.js';
import { hydrate_block_anchor } from '../../hydration.js';
import { remove } from '../../reconciler.js';
import {
current_block,
destroy_signal,
execute_effect,
flushSync,
push_destroy_fn
} from '../../runtime.js';
import { render_effect } from '../../reactivity/effects.js';
import { current_block, execute_effect, flushSync } from '../../runtime.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../../transitions.js';
import { AWAIT_BLOCK, UNINITIALIZED } from '../../constants.js';
@ -74,7 +68,7 @@ export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) {
remove(render.d);
render.d = null;
}
destroy_signal(render.e);
destroy_effect(render.e);
render.e = null;
}
}
@ -181,7 +175,7 @@ export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) {
block,
false
);
push_destroy_fn(await_effect, () => {
await_effect.ondestroy = () => {
let render = current_render;
latest_token = {};
while (render !== null) {
@ -191,10 +185,10 @@ export function await_block(anchor_node, input, pending_fn, then_fn, catch_fn) {
}
const effect = render.e;
if (effect !== null) {
destroy_signal(effect);
destroy_effect(effect);
}
render = render.p;
}
});
};
block.e = await_effect;
}

@ -16,8 +16,8 @@ import {
} from '../../hydration.js';
import { clear_text_content, empty, map_get, map_set } from '../../operations.js';
import { insert, remove } from '../../reconciler.js';
import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js';
import { render_effect } from '../../reactivity/effects.js';
import { current_block, execute_effect } from '../../runtime.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js';
import { trigger_transitions } from '../../transitions.js';
import { is_array, is_frozen } from '../../utils.js';
@ -91,7 +91,7 @@ export function create_each_item_block(item, index, key) {
* @param {() => V[]} collection
* @param {number} flags
* @param {null | ((item: V) => string)} key_fn
* @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal<number>) => void} render_fn
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @returns {void}
@ -133,7 +133,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
remove(fallback.d);
fallback.d = null;
}
destroy_signal(fallback.e);
destroy_effect(fallback.e);
fallback.e = null;
}
}
@ -261,7 +261,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
set_current_hydration_fragment([]);
}
push_destroy_fn(each, () => {
each.ondestroy = () => {
const flags = block.f;
const anchor_node = block.a;
const is_controlled = (flags & EACH_IS_CONTROLLED) !== 0;
@ -273,14 +273,14 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
}
const effect = fallback.e;
if (effect !== null) {
destroy_signal(effect);
destroy_effect(effect);
}
fallback = fallback.p;
}
// Clear the array
reconcile_fn([], block, anchor_node, is_controlled, render_fn, flags, false, keys);
destroy_signal(/** @type {import('../../types.js').Effect} */ (render));
});
destroy_effect(/** @type {import('#client').Effect} */ (render));
};
block.e = each;
}
@ -291,7 +291,7 @@ function each(anchor_node, collection, flags, key_fn, render_fn, fallback_fn, re
* @param {() => V[]} collection
* @param {number} flags
* @param {null | ((item: V) => string)} key_fn
* @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal<number>) => void} render_fn
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void}
*/
@ -304,7 +304,7 @@ export function each_keyed(anchor_node, collection, flags, key_fn, render_fn, fa
* @param {Element | Comment} anchor_node
* @param {() => V[]} collection
* @param {number} flags
* @param {(anchor: null, item: V, index: import('../../types.js').MaybeSignal<number>) => void} render_fn
* @param {(anchor: null, item: V, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn
* @returns {void}
*/
@ -904,7 +904,7 @@ export function destroy_each_item_block(
if (!controlled && dom !== null) {
remove(dom);
}
destroy_signal(/** @type {import('../../types.js').Effect} */ (block.e));
destroy_effect(/** @type {import('#client').Effect} */ (block.e));
}
/**

@ -6,8 +6,8 @@ import {
set_current_hydration_fragment
} from '../../hydration.js';
import { remove } from '../../reconciler.js';
import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js';
import { render_effect } from '../../reactivity/effects.js';
import { current_block, execute_effect } from '../../runtime.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../../transitions.js';
/** @returns {import('../../types.js').IfBlock} */
@ -167,15 +167,15 @@ export function if_block(anchor_node, condition_fn, consequent_fn, alternate_fn)
true
);
block.ae = alternate_effect;
push_destroy_fn(if_effect, () => {
if_effect.ondestroy = () => {
if (consequent_dom !== null) {
remove(consequent_dom);
}
if (alternate_dom !== null) {
remove(alternate_dom);
}
destroy_signal(consequent_effect);
destroy_signal(alternate_effect);
});
destroy_effect(consequent_effect);
destroy_effect(alternate_effect);
};
block.e = if_effect;
}

@ -1,8 +1,8 @@
import { UNINITIALIZED, KEY_BLOCK } from '../../constants.js';
import { hydrate_block_anchor } from '../../hydration.js';
import { remove } from '../../reconciler.js';
import { current_block, destroy_signal, execute_effect, push_destroy_fn } from '../../runtime.js';
import { render_effect } from '../../reactivity/effects.js';
import { current_block, execute_effect } from '../../runtime.js';
import { destroy_effect, render_effect } from '../../reactivity/effects.js';
import { trigger_transitions } from '../../transitions.js';
import { safe_not_equal } from '../../reactivity/equality.js';
@ -58,7 +58,7 @@ export function key_block(anchor_node, key, render_fn) {
remove(render.d);
render.d = null;
}
destroy_signal(render.e);
destroy_effect(render.e);
render.e = null;
}
}
@ -122,7 +122,7 @@ export function key_block(anchor_node, key, render_fn) {
// we trigger the effect after.
render();
mounted = true;
push_destroy_fn(key_effect, () => {
key_effect.ondestroy = () => {
let render = current_render;
while (render !== null) {
const dom = render.d;
@ -131,10 +131,10 @@ export function key_block(anchor_node, key, render_fn) {
}
const effect = render.e;
if (effect !== null) {
destroy_signal(effect);
destroy_effect(effect);
}
render = render.p;
}
});
};
block.e = key_effect;
}

@ -1,11 +1,5 @@
import { DEV } from 'esm-env';
import {
get,
updating_derived,
batch_inspect,
current_component_context,
untrack
} from './runtime.js';
import { get, batch_inspect, current_component_context, untrack } from './runtime.js';
import { effect_active } from './reactivity/effects.js';
import {
array_prototype,
@ -20,6 +14,7 @@ import {
import { add_owner, check_ownership, strip_owner } from './dev/ownership.js';
import { mutable_source, source, set } from './reactivity/sources.js';
import { STATE_SYMBOL, UNINITIALIZED } from './constants.js';
import { updating_derived } from './reactivity/deriveds.js';
/**
* @template T

@ -1,41 +1,52 @@
import { DEV } from 'esm-env';
import { CLEAN, DERIVED, UNINITIALIZED, UNOWNED } from '../constants.js';
import { current_block, current_consumer, current_effect } from '../runtime.js';
import { push_reference } from './effects.js';
import { default_equals, safe_equal } from './equality.js';
import { CLEAN, DERIVED, DESTROYED, DIRTY, MAYBE_DIRTY, UNOWNED } from '../constants.js';
import {
current_reaction,
current_effect,
destroy_children,
remove_reactions,
set_signal_status,
mark_reactions,
current_skip_reaction,
execute_reaction_fn
} from '../runtime.js';
import { equals, safe_equals } from './equality.js';
export let updating_derived = false;
/**
* @template V
* @param {() => V} fn
* @returns {import('../types.js').Derived<V>}
* @returns {import('#client').Derived<V>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function derived(fn) {
let flags = DERIVED | CLEAN;
let flags = DERIVED | DIRTY;
if (current_effect === null) flags |= UNOWNED;
/** @type {import('#client').Derived<V>} */
const signal = {
b: current_block,
c: null,
d: null,
e: default_equals,
reactions: null,
deps: null,
equals,
f: flags,
i: fn,
r: null,
// @ts-expect-error
v: UNINITIALIZED,
w: 0,
x: null,
y: null
fn,
effects: null,
deriveds: null,
v: /** @type {V} */ (null),
version: 0
};
if (DEV) {
/** @type {import('#client').DerivedDebug} */ (signal).inspect = new Set();
}
if (current_consumer !== null) {
push_reference(current_consumer, signal);
if (current_reaction !== null) {
if (current_reaction.deriveds === null) {
current_reaction.deriveds = [signal];
} else {
current_reaction.deriveds.push(signal);
}
}
return signal;
@ -49,6 +60,52 @@ export function derived(fn) {
/*#__NO_SIDE_EFFECTS__*/
export function derived_safe_equal(fn) {
const signal = derived(fn);
signal.e = safe_equal;
signal.equals = safe_equals;
return signal;
}
/**
* @param {import('#client').Derived} derived
* @param {boolean} force_schedule
* @returns {void}
*/
export function update_derived(derived, force_schedule) {
var previous_updating_derived = updating_derived;
updating_derived = true;
destroy_children(derived);
var value = execute_reaction_fn(derived);
updating_derived = previous_updating_derived;
var status =
(current_skip_reaction || (derived.f & UNOWNED) !== 0) && derived.deps !== null
? MAYBE_DIRTY
: CLEAN;
set_signal_status(derived, status);
if (!derived.equals(value)) {
derived.v = value;
mark_reactions(derived, DIRTY, force_schedule);
if (DEV && force_schedule) {
for (var fn of /** @type {import('#client').DerivedDebug} */ (derived).inspect) fn();
}
}
}
/**
* @param {import('#client').Derived} signal
* @returns {void}
*/
export function destroy_derived(signal) {
destroy_children(signal);
remove_reactions(signal, 0);
set_signal_status(signal, DESTROYED);
signal.effects =
signal.deps =
signal.reactions =
// @ts-expect-error `signal.fn` cannot be `null` while the signal is alive
signal.fn =
null;
}

@ -3,63 +3,56 @@ import {
current_block,
current_component_context,
current_effect,
destroy_signal,
current_reaction,
destroy_children,
flush_local_render_effects,
get,
is_runes,
remove_reactions,
schedule_effect,
set_signal_status,
untrack
} from '../runtime.js';
import { DIRTY, MANAGED, RENDER_EFFECT, EFFECT, PRE_EFFECT } from '../constants.js';
import { DIRTY, MANAGED, RENDER_EFFECT, EFFECT, PRE_EFFECT, DESTROYED } from '../constants.js';
import { set } from './sources.js';
/**
* @param {import('#client').Reaction} target_signal
* @param {import('#client').Reaction} ref_signal
* @returns {void}
*/
export function push_reference(target_signal, ref_signal) {
const references = target_signal.r;
if (references === null) {
target_signal.r = [ref_signal];
} else {
references.push(ref_signal);
}
}
/**
* @param {import('./types.js').EffectType} type
* @param {(() => void | (() => void)) | ((b: import('#client').Block) => void | (() => void))} fn
* @param {boolean} sync
* @param {null | import('#client').Block} block
* @param {boolean} schedule
* @param {boolean} init
* @returns {import('#client').Effect}
*/
function create_effect(type, fn, sync, block, schedule) {
function create_effect(type, fn, sync, block = current_block, init = true) {
/** @type {import('#client').Effect} */
const signal = {
b: block,
c: null,
d: null,
e: null,
block,
deps: null,
f: type | DIRTY,
l: 0,
i: fn,
r: null,
v: null,
w: 0,
x: current_component_context,
y: null
fn,
effects: null,
deriveds: null,
teardown: null,
ctx: current_component_context,
ondestroy: null
};
if (current_effect !== null) {
signal.l = current_effect.l + 1;
if ((type & MANAGED) === 0) {
push_reference(current_effect, signal);
}
if ((type & MANAGED) === 0) {
if (current_reaction !== null) {
if (current_reaction.effects === null) {
current_reaction.effects = [signal];
} else {
current_reaction.effects.push(signal);
}
}
}
if (schedule) {
if (init) {
schedule_effect(signal, sync);
}
@ -87,20 +80,17 @@ export function user_effect(fn) {
);
}
const apply_component_effect_heuristics =
// Non-nested `$effect(...)` in a component should be deferred
// until the component is mounted
const defer =
current_effect.f & RENDER_EFFECT &&
// TODO do we actually need this? removing them changes nothing
current_component_context !== null &&
!current_component_context.m;
const effect = create_effect(
EFFECT,
fn,
false,
current_block,
!apply_component_effect_heuristics
);
const effect = create_effect(EFFECT, fn, false, current_block, !defer);
if (apply_component_effect_heuristics) {
if (defer) {
const context = /** @type {import('#client').ComponentContext} */ (current_component_context);
(context.e ??= []).push(effect);
}
@ -116,7 +106,7 @@ export function user_effect(fn) {
export function user_root_effect(fn) {
const effect = render_effect(fn, current_block, true);
return () => {
destroy_signal(effect);
destroy_effect(effect);
};
}
@ -125,7 +115,7 @@ export function user_root_effect(fn) {
* @returns {import('#client').Effect}
*/
export function effect(fn) {
return create_effect(EFFECT, fn, false, current_block, true);
return create_effect(EFFECT, fn, false);
}
/**
@ -133,16 +123,15 @@ export function effect(fn) {
* @returns {import('#client').Effect}
*/
export function managed_effect(fn) {
return create_effect(EFFECT | MANAGED, fn, false, current_block, true);
return create_effect(EFFECT | MANAGED, fn, false);
}
/**
* @param {() => void | (() => void)} fn
* @param {boolean} sync
* @returns {import('#client').Effect}
*/
export function managed_pre_effect(fn, sync) {
return create_effect(PRE_EFFECT | MANAGED, fn, sync, current_block, true);
export function managed_pre_effect(fn) {
return create_effect(PRE_EFFECT | MANAGED, fn, false);
}
/**
@ -159,8 +148,9 @@ export function pre_effect(fn) {
: '')
);
}
const sync = current_effect !== null && (current_effect.f & RENDER_EFFECT) !== 0;
const runes = is_runes(current_component_context);
return create_effect(
PRE_EFFECT,
() => {
@ -168,9 +158,7 @@ export function pre_effect(fn) {
flush_local_render_effects();
return val;
},
sync,
current_block,
true
sync
);
}
@ -196,8 +184,6 @@ export function legacy_pre_effect(deps, fn) {
set(component_context.l2, true);
return untrack(fn);
},
true,
current_block,
true
);
}
@ -223,7 +209,7 @@ export function legacy_pre_effect_reset() {
* @returns {import('#client').Effect}
*/
export function invalidate_effect(fn) {
return create_effect(PRE_EFFECT, fn, true, current_block, true);
return create_effect(PRE_EFFECT, fn, true);
}
/**
@ -239,5 +225,19 @@ export function render_effect(fn, block = current_block, managed = false, sync =
if (managed) {
flags |= MANAGED;
}
return create_effect(flags, /** @type {any} */ (fn), sync, block, true);
return create_effect(flags, /** @type {any} */ (fn), sync, block);
}
/**
* @param {import('#client').Effect} signal
* @returns {void}
*/
export function destroy_effect(signal) {
destroy_children(signal);
remove_reactions(signal, 0);
set_signal_status(signal, DESTROYED);
signal.teardown?.();
signal.ondestroy?.();
signal.fn = signal.effects = signal.ondestroy = signal.ctx = signal.block = signal.deps = null;
}

@ -1,10 +1,6 @@
/**
* @param {unknown} a
* @param {unknown} b
* @returns {boolean}
*/
export function default_equals(a, b) {
return a === b;
/** @type {import('#client').Equals} */
export function equals(value) {
return value === this.v;
}
/**
@ -20,11 +16,7 @@ export function safe_not_equal(a, b) {
: a !== b || (a !== null && typeof a === 'object') || typeof a === 'function';
}
/**
* @param {unknown} a
* @param {unknown} b
* @returns {boolean}
*/
export function safe_equal(a, b) {
return !safe_not_equal(a, b);
/** @type {import('#client').Equals} */
export function safe_equals(value) {
return !safe_not_equal(value, this.v);
}

@ -1,7 +1,7 @@
import { DEV } from 'esm-env';
import {
current_component_context,
current_consumer,
current_reaction,
current_dependencies,
current_effect,
current_untracked_writes,
@ -11,15 +11,15 @@ import {
ignore_mutation_validation,
is_batching_effect,
is_runes,
mark_signal_consumers,
mark_reactions,
schedule_effect,
set_current_untracked_writes,
set_last_inspected_signal,
set_signal_status,
untrack
} from '../runtime.js';
import { default_equals, safe_equal } from './equality.js';
import { CLEAN, DERIVED, DIRTY, MANAGED, SOURCE } from '../constants.js';
import { equals, safe_equals } from './equality.js';
import { CLEAN, DERIVED, DIRTY, MANAGED } from '../constants.js';
/**
* @template V
@ -30,15 +30,15 @@ import { CLEAN, DERIVED, DIRTY, MANAGED, SOURCE } from '../constants.js';
export function source(value) {
/** @type {import('#client').Source<V>} */
const source = {
c: null,
e: default_equals,
f: SOURCE | CLEAN,
f: 0, // TODO ideally we could skip this altogether, but it causes type errors
reactions: null,
equals: equals,
v: value,
w: 0
version: 0
};
if (DEV) {
/** @type {import('#client').SourceDebug<V>} */ (source).inspect = new Set();
/** @type {import('#client').ValueDebug<V>} */ (source).inspect = new Set();
}
return source;
@ -52,7 +52,7 @@ export function source(value) {
/*#__NO_SIDE_EFFECTS__*/
export function mutable_source(initial_value) {
const s = source(initial_value);
s.e = safe_equal;
s.equals = safe_equals;
// bind the signal to the component context, in case we need to
// track updates to trigger beforeUpdate/afterUpdate callbacks
@ -96,9 +96,9 @@ export function set(signal, value) {
if (
!current_untracking &&
!ignore_mutation_validation &&
current_consumer !== null &&
is_runes(null) &&
(current_consumer.f & DERIVED) !== 0
current_reaction !== null &&
is_runes() &&
(current_reaction.f & DERIVED) !== 0
) {
throw new Error(
'ERR_SVELTE_UNSAFE_MUTATION' +
@ -109,16 +109,16 @@ export function set(signal, value) {
: '')
);
}
if (
(signal.f & SOURCE) !== 0 &&
!(/** @type {import('#client').EqualsFunctions} */ (signal.e)(value, signal.v))
) {
if (!signal.equals(value)) {
signal.v = value;
// Increment write version so that unowned signals can properly track dirtyness
signal.w++;
// Increment write version so that unowned signals can properly track dirtiness
signal.version++;
// If the current signal is running for the first time, it won't have any
// consumers as we only allocate and assign the consumers after the signal
// has fully executed. So in the case of ensuring it registers the consumer
// reactions as we only allocate and assign the reactions after the signal
// has fully executed. So in the case of ensuring it registers the reaction
// properly for itself, we need to ensure the current effect actually gets
// scheduled. i.e:
//
@ -127,10 +127,9 @@ export function set(signal, value) {
// We additionally want to skip this logic for when ignore_mutation_validation is
// true, as stores write to source signal on initialisation.
if (
is_runes(null) &&
is_runes() &&
!ignore_mutation_validation &&
current_effect !== null &&
current_effect.c === null &&
(current_effect.f & CLEAN) !== 0 &&
(current_effect.f & MANAGED) === 0
) {
@ -145,10 +144,10 @@ export function set(signal, value) {
}
}
}
mark_signal_consumers(signal, DIRTY, true);
// @ts-expect-error
if (DEV && signal.inspect) {
mark_reactions(signal, DIRTY, true);
if (DEV) {
if (is_batching_effect) {
set_last_inspected_signal(/** @type {import('#client').ValueDebug} */ (signal));
} else {

@ -27,11 +27,6 @@ export function store_get(store, store_name, stores) {
value: mutable_source(UNINITIALIZED),
unsubscribe: noop
};
// TODO: can we remove this code? it was refactored out when we split up source/comptued signals
// push_destroy_fn(entry.value, () => {
// /** @type {import('#client').StoreReferencesContainer['']} */ (entry).last_value =
// /** @type {import('#client').StoreReferencesContainer['']} */ (entry).value.value;
// });
stores[store_name] = entry;
}
@ -110,8 +105,6 @@ export function unsubscribe_on_destroy(stores) {
for (store_name in stores) {
const ref = stores[store_name];
ref.unsubscribe();
// TODO: can we remove this code? it was refactored out when we split up source/comptued signals
// destroy_signal(ref.value);
}
});
}

@ -1,89 +1,61 @@
import type { Block, ComponentContext, EqualsFunctions } from '#client';
import type { DERIVED, EFFECT, PRE_EFFECT, RENDER_EFFECT, SOURCE } from '../constants';
import type { Block, ComponentContext, Equals } from '#client';
import type { EFFECT, PRE_EFFECT, RENDER_EFFECT } from '../constants';
export type SignalFlags =
| typeof SOURCE
| typeof DERIVED
| typeof EFFECT
| typeof PRE_EFFECT
| typeof RENDER_EFFECT;
export type EffectType = typeof EFFECT | typeof PRE_EFFECT | typeof RENDER_EFFECT;
export interface Source<V = unknown> {
/** consumers: Signals that read from the current signal */
c: null | Reaction[];
/** equals: For value equality */
e: null | EqualsFunctions;
/** flags: The types that the signal represent, as a bitwise value */
f: SignalFlags;
/** value: The latest value for this signal */
export interface Signal {
/** Flags bitmask */
f: number;
}
export interface Value<V = unknown> extends Signal {
/** Signals that read from this signal */
reactions: null | Reaction[];
/** Equality function */
equals: Equals;
/** The latest value for this signal */
v: V;
// write version
w: number;
/** Write version */
version: number;
}
export interface SourceDebug<V = unknown> extends Source<V> {
inspect: Set<Function>;
export interface Reaction extends Signal {
/** The reaction function */
fn: null | Function;
/** Signals that this signal reads from */
deps: null | Value[];
/** Effects created inside this signal */
effects: null | Effect[];
/** Deriveds created inside this signal */
deriveds: null | Derived[];
}
export interface Derived<V = unknown> extends Source<V> {
/** dependencies: Signals that this signal reads from */
d: null | Value[];
export interface Derived<V = unknown> extends Value<V>, Reaction {
/** The derived function */
i: () => V;
// TODO get rid of these
fn: () => V;
}
/** references: Anything that a signal owns */
r: null | Reaction[];
/** block: The block associated with this effect/computed */
b: null | Block;
/** context: The associated component if this signal is an effect/computed */
x: null | ComponentContext;
/** destroy: Thing(s) that need destroying */
y: null | (() => void) | Array<() => void>;
export interface Effect extends Reaction {
/** The block associated with this effect */
block: null | Block;
/** The associated component context */
ctx: null | ComponentContext;
/** Stuff to do when the effect is destroyed */
ondestroy: null | (() => void);
/** The effect function */
fn: null | (() => void | (() => void)) | ((b: Block, s: Signal) => void | (() => void));
/** The teardown function returned from the effect function */
teardown: null | (() => void);
/** The depth from the root signal, used for ordering render/pre-effects topologically **/
l: number;
}
export interface DerivedDebug<V = unknown> extends Derived<V> {
export interface ValueDebug<V = unknown> extends Value<V> {
inspect: Set<Function>;
}
export type Effect = {
/** block: The block associated with this effect/computed */
b: null | Block;
/** consumers: Signals that read from the current signal */
c: null | Reaction[];
/** context: The associated component if this signal is an effect/computed */
x: null | ComponentContext;
/** dependencies: Signals that this signal reads from */
d: null | Value[];
/** destroy: Thing(s) that need destroying */
// TODO simplify this, it is only used in one place
y: null | (() => void) | Array<() => void>;
/** equals: For value equality */
e: null | EqualsFunctions;
/** The types that the signal represent, as a bitwise value */
f: SignalFlags;
/** init: The function that we invoke for effects and computeds */
i: null | (() => void | (() => void)) | ((b: Block, s: Signal) => void | (() => void));
/** references: Anything that a signal owns */
r: null | Reaction[];
/** value: The latest value for this signal, doubles as the teardown for effects */
v: null | Function;
/** level: the depth from the root signal, used for ordering render/pre-effects topologically **/
l: number;
/** write version: used for unowned signals to track if their depdendencies are dirty or not **/
w: number;
};
export type Reaction = Derived | Effect;
export type MaybeSignal<T = unknown> = T | Source<T>;
export type UnwrappedSignal<T> = T extends Value<infer U> ? U : T;
export type Value<V = unknown> = Source<V> | Derived<V>;
export interface DerivedDebug<V = unknown> extends Derived<V>, ValueDebug<V> {}
export type ValueDebug<V = unknown> = SourceDebug<V> | DerivedDebug<V>;
export type Source<V = unknown> = Value<V>;
export type Signal = Source | Derived | Effect;
export type MaybeSource<T = unknown> = T | Source<T>;

@ -35,8 +35,6 @@ import {
remove
} from './reconciler.js';
import {
destroy_signal,
push_destroy_fn,
execute_effect,
untrack,
flush_sync,
@ -55,7 +53,8 @@ import {
effect,
managed_effect,
pre_effect,
user_effect
user_effect,
destroy_effect
} from './reactivity/effects.js';
import {
current_hydration_fragment,
@ -76,7 +75,7 @@ import {
import { run } from '../common.js';
import { bind_transition, trigger_transitions } from './transitions.js';
import { mutable_source, source, set } from './reactivity/sources.js';
import { safe_equal, safe_not_equal } from './reactivity/equality.js';
import { safe_equals, safe_not_equal } from './reactivity/equality.js';
import { STATE_SYMBOL } from './constants.js';
/** @type {Set<string>} */
@ -728,11 +727,11 @@ export function bind_playback_rate(media, get_value, update) {
// Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser.
// For hydration we could do it immediately but the additional code is not worth the lost microtask.
/** @type {import('./types.js').Reaction | undefined} */
/** @type {import('./types.js').Effect | undefined} */
let render;
let destroyed = false;
const effect = managed_effect(() => {
destroy_signal(effect);
destroy_effect(effect);
if (destroyed) return;
if (get_value() == null) {
callback();
@ -750,7 +749,7 @@ export function bind_playback_rate(media, get_value, update) {
render_effect(() => () => {
destroyed = true;
if (render) {
destroy_signal(render);
destroy_effect(render);
}
});
}
@ -1382,7 +1381,7 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
// Add effect teardown (likely causes: if block became false, each item removed, component unmounted).
// In these cases we need to nullify the binding only if we detect that the value is still the same.
// If not, that means that another effect has now taken over the binding.
push_destroy_fn(e, () => {
e.ondestroy = () => {
// Defer to the next tick so that all updates can be reconciled first.
// This solves the case where one variable is shared across multiple this-bindings.
effect(() => {
@ -1390,7 +1389,7 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
update(null, ...parts);
}
});
});
};
}
/**
@ -1553,12 +1552,12 @@ export function head(render_fn) {
block,
false
);
push_destroy_fn(head_effect, () => {
head_effect.ondestroy = () => {
const current = block.d;
if (current !== null) {
remove(current);
}
});
};
block.e = head_effect;
} finally {
if (is_hydrating) {
@ -1666,14 +1665,14 @@ export function element(anchor_node, tag_fn, is_svg, render_fn) {
block,
true
);
push_destroy_fn(element_effect, () => {
element_effect.ondestroy = () => {
if (element !== null) {
remove(element);
block.d = null;
element = null;
}
destroy_signal(render_effect_signal);
});
destroy_effect(render_effect_signal);
};
block.e = element_effect;
}
@ -1712,7 +1711,7 @@ export function component(anchor_node, component_fn, render_fn) {
remove(render.d);
render.d = null;
}
destroy_signal(render.e);
destroy_effect(render.e);
render.e = null;
}
}
@ -1779,7 +1778,7 @@ export function component(anchor_node, component_fn, render_fn) {
block,
false
);
push_destroy_fn(component_effect, () => {
component_effect.ondestroy = () => {
let render = current_render;
while (render !== null) {
const dom = render.d;
@ -1788,11 +1787,11 @@ export function component(anchor_node, component_fn, render_fn) {
}
const effect = render.e;
if (effect !== null) {
destroy_signal(effect);
destroy_effect(effect);
}
render = render.p;
}
});
};
block.e = component_effect;
}
@ -1843,9 +1842,9 @@ export function css_props(anchor, is_html, props, component) {
}
current_props = next_props;
});
push_destroy_fn(effect, () => {
effect.ondestroy = () => {
remove(tag);
});
};
}
/**
@ -1875,11 +1874,11 @@ export function html(dom, get_value, svg) {
html_dom = reconcile_html(dom, value, svg);
}
});
push_destroy_fn(effect, () => {
effect.ondestroy = () => {
if (html_dom) {
remove(html_dom);
}
});
};
}
/**
@ -2659,7 +2658,7 @@ function _mount(Component, options) {
if (dom !== null) {
remove(dom);
}
destroy_signal(/** @type {import('./types.js').Effect} */ (block.e));
destroy_effect(/** @type {import('./types.js').Effect} */ (block.e));
});
return component;
@ -2830,7 +2829,7 @@ export function prop(props, key, flags, initial) {
return (inner_current_value.v = parent_value);
});
if (!immutable) current_value.e = safe_equal;
if (!immutable) current_value.equals = safe_equals;
return function (/** @type {V} */ value, mutation = false) {
var current = get(current_value);

@ -1,16 +1,14 @@
import { DEV } from 'esm-env';
import { run_all } from '../common.js';
import {
array_prototype,
get_descriptors,
get_prototype_of,
is_array,
is_frozen,
object_freeze,
object_prototype
} from './utils.js';
import { unstate } from './proxy.js';
import { pre_effect } from './reactivity/effects.js';
import { destroy_effect, pre_effect } from './reactivity/effects.js';
import {
EACH_BLOCK,
IF_BLOCK,
@ -18,7 +16,6 @@ import {
PRE_EFFECT,
RENDER_EFFECT,
DIRTY,
UNINITIALIZED,
MAYBE_DIRTY,
CLEAN,
DERIVED,
@ -31,8 +28,7 @@ import {
import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js';
import { mutate, set, source } from './reactivity/sources.js';
const IS_EFFECT = EFFECT | PRE_EFFECT | RENDER_EFFECT;
import { destroy_derived, update_derived } from './reactivity/deriveds.js';
const FLUSH_MICROTASK = 0;
const FLUSH_SYNC = 1;
@ -55,10 +51,10 @@ let current_queued_pre_and_render_effects = [];
let current_queued_effects = [];
let flush_count = 0;
// Handle signal reactivity tree dependencies and consumer
// Handle signal reactivity tree dependencies and reactions
/** @type {null | import('./types.js').Reaction} */
export let current_consumer = null;
export let current_reaction = null;
/** @type {null | import('./types.js').Effect} */
export let current_effect = null;
@ -96,8 +92,8 @@ export function set_ignore_mutation_validation(value) {
}
// If we are working with a get() chain that has no active container,
// to prevent memory leaks, we skip adding the consumer.
let current_skip_consumer = false;
// to prevent memory leaks, we skip adding the reaction.
export let current_skip_reaction = false;
// Handle collecting all signals which are read during a specific time frame
export let is_signals_recorded = false;
let captured_signals = new Set();
@ -116,15 +112,9 @@ export let current_block = null;
/** @type {import('./types.js').ComponentContext | null} */
export let current_component_context = null;
export let updating_derived = false;
/**
* @param {null | import('./types.js').ComponentContext} context
* @returns {boolean}
*/
export function is_runes(context) {
const component_context = context || current_component_context;
return component_context !== null && component_context.r;
/** @returns {boolean} */
export function is_runes() {
return current_component_context !== null && current_component_context.r;
}
/**
@ -160,51 +150,53 @@ export function batch_inspect(target, prop, receiver) {
}
/**
* @param {import('./types.js').Signal} signal
* Determines whether a derived or effect is dirty.
* If it is MAYBE_DIRTY, will set the status to CLEAN
* @param {import('./types.js').Reaction} reaction
* @returns {boolean}
*/
function is_signal_dirty(signal) {
const flags = signal.f;
if ((flags & DIRTY) !== 0 || signal.v === UNINITIALIZED) {
function check_dirtiness(reaction) {
var flags = reaction.f;
if ((flags & DIRTY) !== 0) {
return true;
}
if ((flags & MAYBE_DIRTY) !== 0) {
const dependencies = /** @type {import('./types.js').Reaction} **/ (signal).d;
var dependencies = reaction.deps;
if (dependencies !== null) {
const length = dependencies.length;
let i;
for (i = 0; i < length; i++) {
const dependency = dependencies[i];
if ((dependency.f & MAYBE_DIRTY) !== 0 && !is_signal_dirty(dependency)) {
set_signal_status(dependency, CLEAN);
continue;
}
// The flags can be marked as dirty from the above is_signal_dirty call.
if ((dependency.f & DIRTY) !== 0) {
if ((dependency.f & DERIVED) !== 0) {
update_derived(/** @type {import('./types.js').Derived} **/ (dependency), true);
// Might have been mutated from above get.
if ((signal.f & DIRTY) !== 0) {
return true;
}
} else {
var length = dependencies.length;
for (var i = 0; i < length; i++) {
var dependency = dependencies[i];
if (check_dirtiness(/** @type {import('#client').Derived} */ (dependency))) {
update_derived(/** @type {import('#client').Derived} **/ (dependency), true);
// `signal` might now be dirty, as a result of calling `update_derived`
if ((reaction.f & DIRTY) !== 0) {
return true;
}
}
// If we're workig with an unowned derived signal, then we need to check
// if our dependency write version is higher. If is is then we can assume
// If we're working with an unowned derived signal, then we need to check
// if our dependency write version is higher. If it is then we can assume
// that state has changed to a newer version and thus this unowned signal
// is also dirty.
const is_unowned = (flags & UNOWNED) !== 0;
const write_version = signal.w;
const dep_write_version = dependency.w;
if (is_unowned && dep_write_version > write_version) {
signal.w = dep_write_version;
var is_unowned = (flags & UNOWNED) !== 0;
var version = dependency.version;
if (is_unowned && version > /** @type {import('#client').Derived} */ (reaction).version) {
/** @type {import('#client').Derived} */ (reaction).version = version;
return true;
}
}
}
set_signal_status(reaction, CLEAN);
}
return false;
}
@ -213,41 +205,40 @@ function is_signal_dirty(signal) {
* @param {import('./types.js').Reaction} signal
* @returns {V}
*/
function execute_signal_fn(signal) {
const init = signal.i;
export function execute_reaction_fn(signal) {
const fn = signal.fn;
const flags = signal.f;
const is_render_effect = (flags & RENDER_EFFECT) !== 0;
const previous_dependencies = current_dependencies;
const previous_dependencies_index = current_dependencies_index;
const previous_untracked_writes = current_untracked_writes;
const previous_consumer = current_consumer;
const previous_block = current_block;
const previous_component_context = current_component_context;
const previous_skip_consumer = current_skip_consumer;
const is_render_effect = (flags & RENDER_EFFECT) !== 0;
const previous_reaction = current_reaction;
const previous_skip_reaction = current_skip_reaction;
const previous_untracking = current_untracking;
current_dependencies = /** @type {null | import('./types.js').Value[]} */ (null);
current_dependencies_index = 0;
current_untracked_writes = null;
current_consumer = signal;
current_block = signal.b;
current_component_context = signal.x;
current_skip_consumer = !is_flushing_effect && (flags & UNOWNED) !== 0;
current_reaction = signal;
current_skip_reaction = !is_flushing_effect && (flags & UNOWNED) !== 0;
current_untracking = false;
try {
let res;
if (is_render_effect) {
res =
/** @type {(block: import('./types.js').Block, signal: import('./types.js').Signal) => V} */ (
init
)(
/** @type {import('./types.js').Block} */ (signal.b),
/** @type {import('./types.js').Signal} */ (signal)
);
res = /** @type {(block: import('#client').Block, signal: import('#client').Signal) => V} */ (
fn
)(
/** @type {import('#client').Block} */ (
/** @type {import('#client').Effect} */ (signal).block
),
/** @type {import('#client').Signal} */ (signal)
);
} else {
res = /** @type {() => V} */ (init)();
res = /** @type {() => V} */ (fn)();
}
let dependencies = /** @type {import('./types.js').Value<unknown>[]} **/ (signal.d);
let dependencies = /** @type {import('./types.js').Value<unknown>[]} **/ (signal.deps);
if (current_dependencies !== null) {
let i;
if (dependencies !== null) {
@ -271,7 +262,7 @@ function execute_signal_fn(signal) {
? !full_current_dependencies_set.has(dependency)
: !full_current_dependencies.includes(dependency)
) {
remove_consumer(signal, dependency);
remove_reaction(signal, dependency);
}
}
}
@ -282,29 +273,29 @@ function execute_signal_fn(signal) {
dependencies[current_dependencies_index + i] = current_dependencies[i];
}
} else {
signal.d = /** @type {import('./types.js').Value<V>[]} **/ (
signal.deps = /** @type {import('./types.js').Value<V>[]} **/ (
dependencies = current_dependencies
);
}
if (!current_skip_consumer) {
if (!current_skip_reaction) {
for (i = current_dependencies_index; i < dependencies.length; i++) {
const dependency = dependencies[i];
const consumers = dependency.c;
const reactions = dependency.reactions;
if (consumers === null) {
dependency.c = [signal];
} else if (consumers[consumers.length - 1] !== signal) {
if (reactions === null) {
dependency.reactions = [signal];
} else if (reactions[reactions.length - 1] !== signal) {
// TODO: should this be:
//
// } else if (!consumers.includes(signal)) {
// } else if (!reactions.includes(signal)) {
//
consumers.push(signal);
reactions.push(signal);
}
}
}
} else if (dependencies !== null && current_dependencies_index < dependencies.length) {
remove_consumers(signal, current_dependencies_index);
remove_reactions(signal, current_dependencies_index);
dependencies.length = current_dependencies_index;
}
return res;
@ -312,10 +303,8 @@ function execute_signal_fn(signal) {
current_dependencies = previous_dependencies;
current_dependencies_index = previous_dependencies_index;
current_untracked_writes = previous_untracked_writes;
current_consumer = previous_consumer;
current_block = previous_block;
current_component_context = previous_component_context;
current_skip_consumer = previous_skip_consumer;
current_reaction = previous_reaction;
current_skip_reaction = previous_skip_reaction;
current_untracking = previous_untracking;
}
}
@ -326,26 +315,26 @@ function execute_signal_fn(signal) {
* @param {import('./types.js').Value<V>} dependency
* @returns {void}
*/
function remove_consumer(signal, dependency) {
const consumers = dependency.c;
let consumers_length = 0;
if (consumers !== null) {
consumers_length = consumers.length - 1;
const index = consumers.indexOf(signal);
function remove_reaction(signal, dependency) {
const reactions = dependency.reactions;
let reactions_length = 0;
if (reactions !== null) {
reactions_length = reactions.length - 1;
const index = reactions.indexOf(signal);
if (index !== -1) {
if (consumers_length === 0) {
dependency.c = null;
if (reactions_length === 0) {
dependency.reactions = null;
} else {
// Swap with last element and then remove.
consumers[index] = consumers[consumers_length];
consumers.pop();
reactions[index] = reactions[reactions_length];
reactions.pop();
}
}
}
if (consumers_length === 0 && (dependency.f & UNOWNED) !== 0) {
if (reactions_length === 0 && (dependency.f & UNOWNED) !== 0) {
// If the signal is unowned then we need to make sure to change it to dirty.
set_signal_status(dependency, DIRTY);
remove_consumers(/** @type {import('./types.js').Reaction} **/ (dependency), 0);
remove_reactions(/** @type {import('./types.js').Derived} **/ (dependency), 0);
}
}
@ -354,16 +343,16 @@ function remove_consumer(signal, dependency) {
* @param {number} start_index
* @returns {void}
*/
function remove_consumers(signal, start_index) {
const dependencies = signal.d;
export function remove_reactions(signal, start_index) {
const dependencies = signal.deps;
if (dependencies !== null) {
const active_dependencies = start_index === 0 ? null : dependencies.slice(0, start_index);
let i;
for (i = start_index; i < dependencies.length; i++) {
const dependency = dependencies[i];
// Avoid removing a consumer if we know that it is active (start_index will not be 0)
// Avoid removing a reaction if we know that it is active (start_index will not be 0)
if (active_dependencies === null || !active_dependencies.includes(dependency)) {
remove_consumer(signal, dependency);
remove_reaction(signal, dependency);
}
}
}
@ -373,28 +362,19 @@ function remove_consumers(signal, start_index) {
* @param {import('./types.js').Reaction} signal
* @returns {void}
*/
function destroy_references(signal) {
const references = signal.r;
signal.r = null;
if (references !== null) {
let i;
for (i = 0; i < references.length; i++) {
destroy_signal(references[i]);
export function destroy_children(signal) {
if (signal.effects) {
for (var i = 0; i < signal.effects.length; i += 1) {
destroy_effect(signal.effects[i]);
}
signal.effects = null;
}
}
/**
* @param {import('./types.js').Block} block
* @param {unknown} error
* @returns {void}
*/
function report_error(block, error) {
/** @type {import('./types.js').Block | null} */
let current_block = block;
if (current_block !== null) {
throw error;
if (signal.deriveds) {
for (i = 0; i < signal.deriveds.length; i += 1) {
destroy_derived(signal.deriveds[i]);
}
signal.deriveds = null;
}
}
@ -406,30 +386,28 @@ export function execute_effect(signal) {
if ((signal.f & DESTROYED) !== 0) {
return;
}
const teardown = signal.v;
const previous_effect = current_effect;
const previous_component_context = current_component_context;
const previous_block = current_block;
const component_context = signal.ctx;
current_effect = signal;
current_component_context = component_context;
current_block = signal.block;
try {
destroy_references(signal);
if (teardown !== null) {
teardown();
}
const possible_teardown = execute_signal_fn(signal);
if (typeof possible_teardown === 'function') {
signal.v = possible_teardown;
}
} catch (error) {
const block = signal.b;
if (block !== null) {
report_error(block, error);
} else {
throw error;
}
destroy_children(signal);
signal.teardown?.();
const teardown = execute_reaction_fn(signal);
signal.teardown = typeof teardown === 'function' ? teardown : null;
} finally {
current_effect = previous_effect;
current_component_context = previous_component_context;
current_block = previous_block;
}
const component_context = signal.x;
if ((signal.f & PRE_EFFECT) !== 0 && current_queued_pre_and_render_effects.length > 0) {
flush_local_pre_effects(component_context);
}
@ -454,31 +432,29 @@ function infinite_loop_guard() {
* @returns {void}
*/
function flush_queued_effects(effects) {
const length = effects.length;
if (length > 0) {
infinite_loop_guard();
const previously_flushing_effect = is_flushing_effect;
is_flushing_effect = true;
try {
let i;
for (i = 0; i < length; i++) {
const signal = effects[i];
const flags = signal.f;
if ((flags & (DESTROYED | INERT)) === 0) {
if (is_signal_dirty(signal)) {
set_signal_status(signal, CLEAN);
execute_effect(signal);
} else if ((flags & MAYBE_DIRTY) !== 0) {
set_signal_status(signal, CLEAN);
}
var length = effects.length;
if (length === 0) return;
infinite_loop_guard();
var previously_flushing_effect = is_flushing_effect;
is_flushing_effect = true;
try {
for (var i = 0; i < length; i++) {
var signal = effects[i];
if ((signal.f & (DESTROYED | INERT)) === 0) {
if (check_dirtiness(signal)) {
set_signal_status(signal, CLEAN);
execute_effect(signal);
}
}
} finally {
is_flushing_effect = previously_flushing_effect;
}
effects.length = 0;
} finally {
is_flushing_effect = previously_flushing_effect;
}
effects.length = 0;
}
function process_microtask() {
@ -540,7 +516,7 @@ export function schedule_effect(signal, sync) {
if (!should_append) {
const target_level = signal.l;
const target_block = signal.b;
const target_block = signal.block;
const is_pre_effect = (flags & PRE_EFFECT) !== 0;
let target_signal;
let is_target_pre_effect;
@ -552,7 +528,10 @@ export function schedule_effect(signal, sync) {
should_append = true;
} else {
is_target_pre_effect = (target_signal.f & PRE_EFFECT) !== 0;
if (target_signal.b !== target_block || (is_target_pre_effect && !is_pre_effect)) {
if (
target_signal.block !== target_block ||
(is_target_pre_effect && !is_pre_effect)
) {
i++;
}
current_queued_pre_and_render_effects.splice(i, 0, signal);
@ -580,7 +559,7 @@ export function flush_local_render_effects() {
const effects = [];
for (let i = 0; i < current_queued_pre_and_render_effects.length; i++) {
const effect = current_queued_pre_and_render_effects[i];
if ((effect.f & RENDER_EFFECT) !== 0 && effect.x === current_component_context) {
if ((effect.f & RENDER_EFFECT) !== 0 && effect.ctx === current_component_context) {
effects.push(effect);
current_queued_pre_and_render_effects.splice(i, 1);
i--;
@ -597,7 +576,7 @@ export function flush_local_pre_effects(context) {
const effects = [];
for (let i = 0; i < current_queued_pre_and_render_effects.length; i++) {
const effect = current_queued_pre_and_render_effects[i];
if ((effect.f & PRE_EFFECT) !== 0 && effect.x === context) {
if ((effect.f & PRE_EFFECT) !== 0 && effect.ctx === context) {
effects.push(effect);
current_queued_pre_and_render_effects.splice(i, 1);
i--;
@ -670,34 +649,6 @@ export async function tick() {
flushSync();
}
/**
* @param {import('./types.js').Derived} signal
* @param {boolean} force_schedule
* @returns {void}
*/
function update_derived(signal, force_schedule) {
const previous_updating_derived = updating_derived;
updating_derived = true;
destroy_references(signal);
const value = execute_signal_fn(signal);
updating_derived = previous_updating_derived;
const status =
(current_skip_consumer || (signal.f & UNOWNED) !== 0) && signal.d !== null
? MAYBE_DIRTY
: CLEAN;
set_signal_status(signal, status);
const equals = /** @type {import('./types.js').EqualsFunctions} */ (signal.e);
if (!equals(value, signal.v)) {
signal.v = value;
mark_signal_consumers(signal, DIRTY, force_schedule);
// @ts-expect-error
if (DEV && signal.inspect && force_schedule) {
for (const fn of /** @type {import('./types.js').ValueDebug} */ (signal).inspect) fn();
}
}
}
/**
* @template V
* @param {import('./types.js').Value<V>} signal
@ -720,10 +671,10 @@ export function get(signal) {
captured_signals.add(signal);
}
// Register the dependency on the current consumer signal.
if (current_consumer !== null && (current_consumer.f & MANAGED) === 0 && !current_untracking) {
const unowned = (current_consumer.f & UNOWNED) !== 0;
const dependencies = current_consumer.d;
// Register the dependency on the current reaction signal.
if (current_reaction !== null && (current_reaction.f & MANAGED) === 0 && !current_untracking) {
const unowned = (current_reaction.f & UNOWNED) !== 0;
const dependencies = current_reaction.deps;
if (
current_dependencies === null &&
dependencies !== null &&
@ -754,7 +705,10 @@ export function get(signal) {
}
}
if ((flags & DERIVED) !== 0 && is_signal_dirty(signal)) {
if (
(flags & DERIVED) !== 0 &&
check_dirtiness(/** @type {import('#client').Derived} */ (signal))
) {
if (DEV) {
// we want to avoid tracking indirect dependencies
const previous_inspect_fn = inspect_fn;
@ -797,28 +751,25 @@ export function invalidate_inner_signals(fn) {
}
/**
* @param {import('./types.js').Reaction} signal
* @param {import('#client').Effect} signal
* @param {boolean} inert
* @param {Set<import('./types.js').Block>} [visited_blocks]
* @param {Set<import('#client').Block>} [visited_blocks]
* @returns {void}
*/
function mark_subtree_children_inert(signal, inert, visited_blocks) {
const references = signal.r;
if (references !== null) {
let i;
for (i = 0; i < references.length; i++) {
const reference = references[i];
if ((reference.f & IS_EFFECT) !== 0) {
mark_subtree_inert(reference, inert, visited_blocks);
}
const effects = signal.effects;
if (effects !== null) {
for (var i = 0; i < effects.length; i++) {
const effect = effects[i];
mark_subtree_inert(effect, inert, visited_blocks);
}
}
}
/**
* @param {import('./types.js').Reaction} signal
* @param {import('#client').Effect} signal
* @param {boolean} inert
* @param {Set<import('./types.js').Block>} [visited_blocks]
* @param {Set<import('#client').Block>} [visited_blocks]
* @returns {void}
*/
export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) {
@ -826,11 +777,11 @@ export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) {
const is_already_inert = (flags & INERT) !== 0;
if (is_already_inert !== inert) {
signal.f ^= INERT;
if (!inert && (flags & IS_EFFECT) !== 0 && (flags & CLEAN) === 0) {
schedule_effect(/** @type {import('./types.js').Effect} */ (signal), false);
if (!inert && (flags & CLEAN) === 0) {
schedule_effect(signal, false);
}
// Nested if block effects
const block = signal.b;
const block = signal.block;
if (block !== null && !visited_blocks.has(block)) {
visited_blocks.add(block);
const type = block.t;
@ -861,65 +812,49 @@ export function mark_subtree_inert(signal, inert, visited_blocks = new Set()) {
}
/**
* @param {import('./types.js').Signal} signal
* @param {import('#client').Value} signal
* @param {number} to_status
* @param {boolean} force_schedule
* @returns {void}
*/
export function mark_signal_consumers(signal, to_status, force_schedule) {
const runes = is_runes(null);
const consumers = signal.c;
if (consumers !== null) {
const length = consumers.length;
let i;
for (i = 0; i < length; i++) {
const consumer = consumers[i];
const flags = consumer.f;
const unowned = (flags & UNOWNED) !== 0;
// We skip any effects that are already dirty (but not unowned). Additionally, we also
// skip if the consumer is the same as the current effect (except if we're not in runes or we
// are in force schedule mode).
if ((!force_schedule || !runes) && consumer === current_effect) {
continue;
}
set_signal_status(consumer, to_status);
// If the signal is not clean, then skip over it with the exception of unowned signals that
// are already maybe dirty. Unowned signals might be dirty because they are not captured as part of an
// effect.
const maybe_dirty = (flags & MAYBE_DIRTY) !== 0;
if ((flags & CLEAN) !== 0 || (maybe_dirty && unowned)) {
if ((consumer.f & IS_EFFECT) !== 0) {
schedule_effect(/** @type {import('./types.js').Effect} */ (consumer), false);
} else {
mark_signal_consumers(consumer, MAYBE_DIRTY, force_schedule);
}
}
export function mark_reactions(signal, to_status, force_schedule) {
var reactions = signal.reactions;
if (reactions === null) return;
var runes = is_runes();
var length = reactions.length;
for (var i = 0; i < length; i++) {
var reaction = reactions[i];
// We skip any effects that are already dirty (but not unowned). Additionally, we also
// skip if the reaction is the same as the current effect (except if we're not in runes or we
// are in force schedule mode).
if ((!force_schedule || !runes) && reaction === current_effect) {
continue;
}
}
}
/**
* @param {import('./types.js').Reaction} signal
* @returns {void}
*/
export function destroy_signal(signal) {
const teardown = /** @type {null | (() => void)} */ (signal.v);
const destroy = signal.y;
const flags = signal.f;
destroy_references(signal);
remove_consumers(signal, 0);
signal.i = signal.r = signal.y = signal.x = signal.b = signal.d = signal.c = null;
set_signal_status(signal, DESTROYED);
if (destroy !== null) {
if (is_array(destroy)) {
run_all(destroy);
} else {
destroy();
var flags = reaction.f;
set_signal_status(reaction, to_status);
// If the signal is not clean, then skip over it with the exception of unowned signals that
// are already maybe dirty. Unowned signals might be dirty because they are not captured as part of an
// effect.
var maybe_dirty = (flags & MAYBE_DIRTY) !== 0;
var unowned = (flags & UNOWNED) !== 0;
if ((flags & CLEAN) !== 0 || (maybe_dirty && unowned)) {
if ((reaction.f & DERIVED) !== 0) {
mark_reactions(
/** @type {import('#client').Derived} */ (reaction),
MAYBE_DIRTY,
force_schedule
);
} else {
schedule_effect(/** @type {import('#client').Effect} */ (reaction), false);
}
}
}
if (teardown !== null && (flags & IS_EFFECT) !== 0) {
teardown();
}
}
/**
@ -940,22 +875,6 @@ export function untrack(fn) {
}
}
/**
* @param {import('./types.js').Reaction} signal
* @param {() => void} destroy_fn
* @returns {void}
*/
export function push_destroy_fn(signal, destroy_fn) {
let destroy = signal.y;
if (destroy === null) {
signal.y = destroy_fn;
} else if (is_array(destroy)) {
destroy.push(destroy_fn);
} else {
signal.y = [destroy, destroy_fn];
}
}
const STATUS_MASK = ~(DIRTY | MAYBE_DIRTY | CLEAN);
/**
@ -1350,8 +1269,8 @@ export function inspect(get_value, inspect = console.log) {
/**
* @template V
* @param {V} value
* @returns {import('./types.js').UnwrappedSignal<V>}
* @param {V | import('#client').Value<V>} value
* @returns {V}
*/
export function unwrap(value) {
if (is_signal(value)) {

@ -12,11 +12,15 @@ import {
import { destroy_each_item_block, get_first_element } from './dom/blocks/each.js';
import { schedule_raf_task } from './dom/task.js';
import { append_child, empty } from './operations.js';
import { effect, managed_effect, managed_pre_effect } from './reactivity/effects.js';
import {
destroy_effect,
effect,
managed_effect,
managed_pre_effect
} from './reactivity/effects.js';
import {
current_block,
current_effect,
destroy_signal,
execute_effect,
mark_subtree_inert,
untrack
@ -589,7 +593,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
}
const effect = managed_pre_effect(() => {
destroy_signal(effect);
destroy_effect(effect);
dom.inert = false;
if (show_intro && !already_mounted) {
@ -613,7 +617,7 @@ export function bind_transition(dom, get_transition_fn, props_fn, direction, glo
}
transition_block = parent;
}
}, false);
});
});
if (direction === 'key') {
@ -666,12 +670,12 @@ export function trigger_transitions(transitions, target_direction, from) {
if (outros.length > 0) {
// Defer the outros to a microtask
const e = managed_pre_effect(() => {
destroy_signal(e);
destroy_effect(e);
const e2 = managed_effect(() => {
destroy_signal(e2);
destroy_effect(e2);
run_all(outros);
});
}, false);
});
}
}

@ -11,7 +11,7 @@ import {
SNIPPET_BLOCK,
STATE_SYMBOL
} from './constants.js';
import type { Reaction, Effect, Signal, Source, Value } from './reactivity/types.js';
import type { Effect, Source, Value } from './reactivity/types.js';
type EventCallback = (event: Event) => boolean;
export type EventCallbackMap = Record<string, EventCallback | EventCallback[]>;
@ -57,7 +57,7 @@ export type ComponentContext = {
};
};
export type EqualsFunctions<T = any> = (a: T, v: T) => boolean;
export type Equals = (this: Value, value: unknown) => boolean;
export type BlockType =
| typeof ROOT_BLOCK
@ -99,7 +99,7 @@ export type RootBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Reaction;
e: null | Effect;
/** intro */
i: boolean;
/** parent */
@ -150,7 +150,7 @@ export type HeadBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Reaction;
e: null | Effect;
/** parent */
p: Block;
/** transition */
@ -163,7 +163,7 @@ export type DynamicElementBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Reaction;
e: null | Effect;
/** parent */
p: Block;
/** transition */
@ -176,7 +176,7 @@ export type DynamicComponentBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Reaction;
e: null | Effect;
/** parent */
p: Block;
/** transition */
@ -189,7 +189,7 @@ export type AwaitBlock = {
/** dom */
d: null | TemplateNode | Array<TemplateNode>;
/** effect */
e: null | Reaction;
e: null | Effect;
/** parent */
p: Block;
/** pending */
@ -210,7 +210,7 @@ export type EachBlock = {
/** items */
v: EachItemBlock[];
/** effewct */
e: null | Reaction;
e: null | Effect;
/** parent */
p: Block;
/** transition */
@ -250,7 +250,7 @@ export type SnippetBlock = {
/** parent */
p: Block;
/** effect */
e: null | Reaction;
e: null | Effect;
/** transition */
r: null;
/** type */

@ -1,6 +1,11 @@
import { describe, assert, it } from 'vitest';
import * as $ from '../../src/internal/client/runtime';
import { effect, render_effect, user_effect } from '../../src/internal/client/reactivity/effects';
import {
destroy_effect,
effect,
render_effect,
user_effect
} from '../../src/internal/client/reactivity/effects';
import { source, set } from '../../src/internal/client/reactivity/sources';
import type { Derived } from '../../src/internal/client/types';
import { proxy } from '../../src/internal/client/proxy';
@ -27,7 +32,7 @@ function run_test(runes: boolean, fn: (runes: boolean) => () => void) {
);
$.pop();
execute();
$.destroy_signal(signal);
destroy_effect(signal);
};
}
@ -193,13 +198,13 @@ describe('signals', () => {
return () => {
$.flushSync(() => set(count, 1));
// Ensure we're not leaking consumers
assert.deepEqual(count.c?.length, 1);
assert.deepEqual(count.reactions?.length, 1);
$.flushSync(() => set(count, 2));
// Ensure we're not leaking consumers
assert.deepEqual(count.c?.length, 1);
assert.deepEqual(count.reactions?.length, 1);
$.flushSync(() => set(count, 3));
// Ensure we're not leaking consumers
assert.deepEqual(count.c?.length, 1);
assert.deepEqual(count.reactions?.length, 1);
assert.deepEqual(log, [0, 1, 2, 3]);
};
});
@ -230,7 +235,7 @@ describe('signals', () => {
// Ensure we're not leaking dependencies
assert.deepEqual(
nested.slice(0, -2).map((s) => s.d),
nested.slice(0, -2).map((s) => s.deps),
[null, null]
);
};
@ -259,11 +264,11 @@ describe('signals', () => {
$.flushSync(() => set(count, 4));
$.flushSync(() => set(count, 0));
// Ensure we're not leaking consumers
assert.deepEqual(count.c?.length, 1);
assert.deepEqual(count.reactions?.length, 1);
assert.deepEqual(log, [0, 2, 'limit', 0]);
$.destroy_signal(effect);
destroy_effect(effect);
// Ensure we're not leaking consumers
assert.deepEqual(count.c, null);
assert.deepEqual(count.reactions, null);
};
});
@ -351,4 +356,26 @@ describe('signals', () => {
assert.equal(errored, true);
};
});
test('effect teardown is removed on re-run', () => {
const count = source(0);
let first = true;
let teardown = 0;
user_effect(() => {
$.get(count);
if (first) {
first = false;
return () => {
teardown += 1;
};
}
});
return () => {
$.flushSync(() => set(count, 1));
$.flushSync(() => set(count, 2));
assert.equal(teardown, 1);
};
});
});

Loading…
Cancel
Save