chore: refactor effects a bit (#10948)

* WIP

* formalise branch effects

* WIP

* rename MANAGED to BRANCH_EFFECT

* remove ondestroy functions

* tidy up

* simplify

* lint

* tidy up

* tidy up

* tidy up

* tidy up

* remove ondestroy

* tidy up

* tidy up

* remove TODO comment

* update comment
pull/10955/head
Rich Harris 1 year ago committed by GitHub
parent b6598a3cc5
commit 9a4cd7e8d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -389,7 +389,7 @@ export const javascript_visitors_runes = {
const args = /** @type {import('estree').Expression[]} */ ( const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => context.visit(arg)) node.arguments.map((arg) => context.visit(arg))
); );
return b.call('$.user_root_effect', ...args); return b.call('$.effect_root', ...args);
} }
if (rune === '$inspect' || rune === '$inspect().with') { if (rune === '$inspect' || rune === '$inspect().with') {

@ -2,7 +2,8 @@ export const DERIVED = 1 << 1;
export const EFFECT = 1 << 2; export const EFFECT = 1 << 2;
export const PRE_EFFECT = 1 << 3; export const PRE_EFFECT = 1 << 3;
export const RENDER_EFFECT = 1 << 4; export const RENDER_EFFECT = 1 << 4;
export const MANAGED = 1 << 6; export const BLOCK_EFFECT = 1 << 5;
export const BRANCH_EFFECT = 1 << 6;
export const UNOWNED = 1 << 7; export const UNOWNED = 1 << 7;
export const CLEAN = 1 << 8; export const CLEAN = 1 << 8;
export const DIRTY = 1 << 9; export const DIRTY = 1 << 9;
@ -11,6 +12,7 @@ export const INERT = 1 << 11;
export const DESTROYED = 1 << 12; export const DESTROYED = 1 << 12;
export const IS_ELSEIF = 1 << 13; export const IS_ELSEIF = 1 << 13;
export const EFFECT_RAN = 1 << 14; export const EFFECT_RAN = 1 << 14;
export const ROOT_EFFECT = 1 << 15;
export const UNINITIALIZED = Symbol(); export const UNINITIALIZED = Symbol();
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');

@ -1,5 +1,4 @@
import { is_promise } from '../../../common.js'; import { is_promise } from '../../../common.js';
import { remove } from '../reconciler.js';
import { import {
current_component_context, current_component_context,
flushSync, flushSync,
@ -7,7 +6,7 @@ import {
set_current_effect, set_current_effect,
set_current_reaction set_current_reaction
} from '../../runtime.js'; } from '../../runtime.js';
import { destroy_effect, pause_effect, render_effect } from '../../reactivity/effects.js'; import { block, branch, destroy_effect, pause_effect } from '../../reactivity/effects.js';
import { INERT } from '../../constants.js'; import { INERT } from '../../constants.js';
/** /**
@ -39,10 +38,10 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
* @param {any} value * @param {any} value
*/ */
function create_effect(fn, value) { function create_effect(fn, value) {
set_current_effect(branch); set_current_effect(effect);
set_current_reaction(branch); // TODO do we need both? set_current_reaction(effect); // TODO do we need both?
set_current_component_context(component_context); set_current_component_context(component_context);
var effect = render_effect(() => fn(anchor, value), true); var e = branch(() => fn(anchor, value));
set_current_component_context(null); set_current_component_context(null);
set_current_reaction(null); set_current_reaction(null);
set_current_effect(null); set_current_effect(null);
@ -51,10 +50,10 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
// resolves which is unexpected behaviour (and somewhat irksome to test) // resolves which is unexpected behaviour (and somewhat irksome to test)
flushSync(); flushSync();
return effect; return e;
} }
const branch = render_effect(() => { const effect = block(() => {
if (input === (input = get_input())) return; if (input === (input = get_input())) return;
if (is_promise(input)) { if (is_promise(input)) {
@ -62,11 +61,10 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
if (pending_fn) { if (pending_fn) {
if (pending_effect && (pending_effect.f & INERT) === 0) { if (pending_effect && (pending_effect.f & INERT) === 0) {
if (pending_effect.dom) remove(pending_effect.dom);
destroy_effect(pending_effect); destroy_effect(pending_effect);
} }
pending_effect = render_effect(() => pending_fn(anchor), true); pending_effect = branch(() => pending_fn(anchor));
} }
if (then_effect) pause_effect(then_effect); if (then_effect) pause_effect(then_effect);
@ -96,19 +94,11 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
if (then_fn) { if (then_fn) {
if (then_effect) { if (then_effect) {
if (then_effect.dom) remove(then_effect.dom);
destroy_effect(then_effect); destroy_effect(then_effect);
} }
then_effect = render_effect(() => then_fn(anchor, input), true); then_effect = branch(() => then_fn(anchor, input));
} }
} }
}); });
branch.ondestroy = () => {
// TODO this sucks, tidy it up
if (pending_effect?.dom) remove(pending_effect.dom);
if (then_effect?.dom) remove(then_effect.dom);
if (catch_effect?.dom) remove(catch_effect.dom);
};
} }

@ -39,10 +39,11 @@ export function css_props(anchor, is_html, props, component) {
component(component_anchor); component(component_anchor);
render_effect(() => {
/** @type {Record<string, string>} */ /** @type {Record<string, string>} */
let current_props = {}; let current_props = {};
const effect = render_effect(() => { render_effect(() => {
const next_props = props(); const next_props = props();
for (const key in current_props) { for (const key in current_props) {
@ -58,7 +59,8 @@ export function css_props(anchor, is_html, props, component) {
current_props = next_props; current_props = next_props;
}); });
effect.ondestroy = () => { return () => {
remove(element); remove(element);
}; };
});
} }

@ -11,11 +11,11 @@ import { empty } from '../operations.js';
import { insert, remove } from '../reconciler.js'; import { insert, remove } from '../reconciler.js';
import { untrack } from '../../runtime.js'; import { untrack } from '../../runtime.js';
import { import {
destroy_effect, block,
branch,
effect, effect,
pause_effect, pause_effect,
pause_effects, pause_effects,
render_effect,
resume_effect resume_effect
} from '../../reactivity/effects.js'; } from '../../reactivity/effects.js';
import { source, mutable_source, set } from '../../reactivity/sources.js'; import { source, mutable_source, set } from '../../reactivity/sources.js';
@ -43,7 +43,7 @@ export function set_current_each_item(item) {
* @param {number} flags * @param {number} flags
* @param {() => V[]} get_collection * @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key * @param {null | ((item: V) => string)} get_key
* @param {(anchor: Node, item: V, index: import('#client').MaybeSource<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} fallback_fn * @param {null | ((anchor: Node) => void)} fallback_fn
* @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn * @param {typeof reconcile_indexed_array | reconcile_tracked_array} reconcile_fn
* @returns {void} * @returns {void}
@ -67,7 +67,7 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
/** @type {import('#client').Effect | null} */ /** @type {import('#client').Effect | null} */
var fallback = null; var fallback = null;
var effect = render_effect(() => { block(() => {
var collection = get_collection(); var collection = get_collection();
var array = is_array(collection) var array = is_array(collection)
@ -152,15 +152,7 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
if (fallback) { if (fallback) {
resume_effect(fallback); resume_effect(fallback);
} else { } else {
fallback = render_effect(() => { fallback = branch(() => fallback_fn(anchor));
var dom = fallback_fn(anchor);
return () => {
if (dom !== undefined) {
remove(dom);
}
};
}, true);
} }
} else if (fallback !== null) { } else if (fallback !== null) {
pause_effect(fallback, () => { pause_effect(fallback, () => {
@ -174,17 +166,6 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
set_hydrating(true); set_hydrating(true);
} }
}); });
effect.ondestroy = () => {
for (var item of state.items) {
if (item.e.dom !== null) {
remove(item.e.dom);
destroy_effect(item.e);
}
}
if (fallback) destroy_effect(fallback);
};
} }
/** /**
@ -193,7 +174,7 @@ function each(anchor, flags, get_collection, get_key, render_fn, fallback_fn, re
* @param {number} flags * @param {number} flags
* @param {() => V[]} get_collection * @param {() => V[]} get_collection
* @param {null | ((item: V) => string)} get_key * @param {null | ((item: V) => string)} get_key
* @param {(anchor: Node, item: V, index: import('#client').MaybeSource<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} [fallback_fn] * @param {null | ((anchor: Node) => void)} [fallback_fn]
* @returns {void} * @returns {void}
*/ */
@ -206,7 +187,7 @@ export function each_keyed(anchor, flags, get_collection, get_key, render_fn, fa
* @param {Element | Comment} anchor * @param {Element | Comment} anchor
* @param {number} flags * @param {number} flags
* @param {() => V[]} get_collection * @param {() => V[]} get_collection
* @param {(anchor: Node, item: V, index: import('#client').MaybeSource<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: import('#client').MaybeSource<number>) => void} render_fn
* @param {null | ((anchor: Node) => void)} [fallback_fn] * @param {null | ((anchor: Node) => void)} [fallback_fn]
* @returns {void} * @returns {void}
*/ */
@ -219,7 +200,7 @@ export function each_indexed(anchor, flags, get_collection, render_fn, fallback_
* @param {Array<V>} array * @param {Array<V>} array
* @param {import('#client').EachState} state * @param {import('#client').EachState} state
* @param {Element | Comment | Text} anchor * @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: V, index: number | import('#client').Source<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @returns {void} * @returns {void}
*/ */
@ -275,7 +256,7 @@ function reconcile_indexed_array(array, state, anchor, render_fn, flags) {
* @param {Array<V>} array * @param {Array<V>} array
* @param {import('#client').EachState} state * @param {import('#client').EachState} state
* @param {Element | Comment | Text} anchor * @param {Element | Comment | Text} anchor
* @param {(anchor: Node, item: V, index: number | import('#client').Source<number>) => void} render_fn * @param {(anchor: Node, item: import('#client').MaybeSource<V>, index: number | import('#client').Source<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @param {any[]} keys * @param {any[]} keys
* @returns {void} * @returns {void}
@ -342,7 +323,6 @@ function reconcile_tracked_array(array, state, anchor, render_fn, flags, keys) {
var i; var i;
var index; var index;
var last_item; var last_item;
var last_sibling;
// store the indexes of each item in the new world // store the indexes of each item in the new world
for (i = start; i < b; i += 1) { for (i = start; i < b; i += 1) {
@ -548,45 +528,32 @@ function update_item(item, value, index, type) {
* @param {V} value * @param {V} value
* @param {unknown} key * @param {unknown} key
* @param {number} index * @param {number} index
* @param {(anchor: Node, item: V, index: number | import('#client').Value<number>) => void} render_fn * @param {(anchor: Node, item: V | import('#client').Source<V>, index: number | import('#client').Value<number>) => void} render_fn
* @param {number} flags * @param {number} flags
* @returns {import('#client').EachItem} * @returns {import('#client').EachItem}
*/ */
function create_item(anchor, value, key, index, render_fn, flags) { function create_item(anchor, value, key, index, render_fn, flags) {
var each_item_not_reactive = (flags & EACH_ITEM_REACTIVE) === 0; var previous_each_item = current_each_item;
try {
var reactive = (flags & EACH_ITEM_REACTIVE) !== 0;
var mutable = (flags & EACH_IS_STRICT_EQUALS) === 0;
var v = reactive ? (mutable ? mutable_source(value) : source(value)) : value;
var i = (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index);
/** @type {import('#client').EachItem} */ /** @type {import('#client').EachItem} */
var item = { var item = {
i,
v,
k: key,
a: null, a: null,
// dom
// @ts-expect-error // @ts-expect-error
e: null, e: null
// index
i: (flags & EACH_INDEX_REACTIVE) === 0 ? index : source(index),
// key
k: key,
// item
v: each_item_not_reactive
? value
: (flags & EACH_IS_STRICT_EQUALS) !== 0
? source(value)
: mutable_source(value)
}; };
var previous_each_item = current_each_item;
try {
current_each_item = item; current_each_item = item;
item.e = branch(() => render_fn(anchor, v, i));
item.e = render_effect(() => {
var dom = render_fn(anchor, item.v, item.i);
return () => {
if (dom !== undefined) {
remove(dom);
}
};
}, true);
return item; return item;
} finally { } finally {

@ -1,29 +1,22 @@
import { derived } from '../../reactivity/deriveds.js';
import { render_effect } from '../../reactivity/effects.js'; import { render_effect } from '../../reactivity/effects.js';
import { get } from '../../runtime.js';
import { reconcile_html, remove } from '../reconciler.js'; import { reconcile_html, remove } from '../reconciler.js';
/** /**
* @param {Element | Text | Comment} dom * @param {Element | Text | Comment} anchor
* @param {() => string} get_value * @param {() => string} get_value
* @param {boolean} svg * @param {boolean} svg
* @returns {void} * @returns {void}
*/ */
export function html(dom, get_value, svg) { export function html(anchor, get_value, svg) {
/** @type {import('#client').Dom} */ let value = derived(get_value);
let html_dom;
/** @type {string} */ render_effect(() => {
let value; var dom = reconcile_html(anchor, get(value), svg);
const effect = render_effect(() => { if (dom) {
if (value !== (value = get_value())) { return () => remove(dom);
if (html_dom) remove(html_dom);
html_dom = reconcile_html(dom, value, svg);
} }
}); });
effect.ondestroy = () => {
if (html_dom) {
remove(html_dom);
}
};
} }

@ -1,12 +1,7 @@
import { IS_ELSEIF } from '../../constants.js'; import { IS_ELSEIF } from '../../constants.js';
import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js'; import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
import { remove } from '../reconciler.js'; import { remove } from '../reconciler.js';
import { import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
destroy_effect,
pause_effect,
render_effect,
resume_effect
} from '../../reactivity/effects.js';
/** /**
* @param {Comment} anchor * @param {Comment} anchor
@ -32,7 +27,7 @@ export function if_block(
/** @type {boolean | null} */ /** @type {boolean | null} */
let condition = null; let condition = null;
const if_effect = render_effect(() => { const effect = block(() => {
if (condition === (condition = !!get_condition())) return; if (condition === (condition = !!get_condition())) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */ /** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@ -61,7 +56,7 @@ export function if_block(
if (consequent_effect) { if (consequent_effect) {
resume_effect(consequent_effect); resume_effect(consequent_effect);
} else { } else {
consequent_effect = render_effect(() => consequent_fn(anchor), true); consequent_effect = branch(() => consequent_fn(anchor));
} }
if (alternate_effect) { if (alternate_effect) {
@ -73,7 +68,7 @@ export function if_block(
if (alternate_effect) { if (alternate_effect) {
resume_effect(alternate_effect); resume_effect(alternate_effect);
} else if (alternate_fn) { } else if (alternate_fn) {
alternate_effect = render_effect(() => alternate_fn(anchor), true); alternate_effect = branch(() => alternate_fn(anchor));
} }
if (consequent_effect) { if (consequent_effect) {
@ -90,17 +85,6 @@ export function if_block(
}); });
if (elseif) { if (elseif) {
if_effect.f |= IS_ELSEIF; effect.f |= IS_ELSEIF;
} }
if_effect.ondestroy = () => {
// TODO why is this not automatic? this should be children of `if_effect`
if (consequent_effect) {
destroy_effect(consequent_effect);
}
if (alternate_effect) {
destroy_effect(alternate_effect);
}
};
} }

@ -1,6 +1,5 @@
import { UNINITIALIZED } from '../../constants.js'; import { UNINITIALIZED } from '../../constants.js';
import { remove } from '../reconciler.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { pause_effect, render_effect } from '../../reactivity/effects.js';
import { safe_not_equal } from '../../reactivity/equality.js'; import { safe_not_equal } from '../../reactivity/equality.js';
/** /**
@ -17,39 +16,13 @@ export function key_block(anchor, get_key, render_fn) {
/** @type {import('#client').Effect} */ /** @type {import('#client').Effect} */
let effect; let effect;
/** block(() => {
* Every time `key` changes, we create a new effect. Old effects are
* removed from this set when they have fully transitioned out
* @type {Set<import('#client').Effect>}
*/
let effects = new Set();
const key_effect = render_effect(() => {
if (safe_not_equal(key, (key = get_key()))) { if (safe_not_equal(key, (key = get_key()))) {
if (effect) { if (effect) {
var e = effect; pause_effect(effect);
pause_effect(e, () => {
effects.delete(e);
});
}
effect = render_effect(() => {
const dom = render_fn(anchor);
return () => {
if (dom !== undefined) {
remove(dom);
} }
};
}, true);
effects.add(effect); effect = branch(() => render_fn(anchor));
} }
}); });
key_effect.ondestroy = () => {
for (const e of effects) {
if (e.dom) remove(e.dom);
}
};
} }

@ -20,12 +20,10 @@ export function snippet(get_snippet, node, ...args) {
// Untrack so we only rerender when the snippet function itself changes, // Untrack so we only rerender when the snippet function itself changes,
// not when an eagerly-read prop inside the snippet function changes // not when an eagerly-read prop inside the snippet function changes
var dom = untrack(() => /** @type {SnippetFn} */ (snippet_fn)(node, ...args)); var dom = untrack(() => /** @type {SnippetFn} */ (snippet_fn)(node, ...args));
}
return () => {
if (dom !== undefined) { if (dom !== undefined) {
remove(dom); return () => remove(dom);
}
} }
};
}); });
} }

@ -1,8 +1,6 @@
import { pause_effect, render_effect } from '../../reactivity/effects.js'; import { block, branch, pause_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { current_effect } from '../../runtime.js';
// TODO this is very similar to `key`, can we deduplicate? // TODO seems weird that `anchor` is unused here — possible bug?
/** /**
* @template P * @template P
@ -16,46 +14,19 @@ export function component(anchor, get_component, render_fn) {
/** @type {C} */ /** @type {C} */
let component; let component;
/** @type {import('#client').Effect} */ /** @type {import('#client').Effect | null} */
let effect; let effect;
/** block(() => {
* Every time `component` changes, we create a new effect. Old effects are
* removed from this set when they have fully transitioned out
* @type {Set<import('#client').Effect>}
*/
let effects = new Set();
const component_effect = render_effect(() => {
if (component === (component = get_component())) return; if (component === (component = get_component())) return;
if (effect) { if (effect) {
var e = effect; pause_effect(effect);
pause_effect(e, () => { effect = null;
effects.delete(e);
});
} }
if (component) { if (component) {
effect = render_effect(() => { effect = branch(() => render_fn(component));
render_fn(component);
// `render_fn` doesn't return anything, and we can't reference `effect`
// yet, so we reference it indirectly as `current_effect`
const dom = /** @type {import('#client').Effect} */ (current_effect).dom;
return () => {
if (dom !== null) remove(dom);
};
}, true);
effects.add(effect);
} }
}); });
component_effect.ondestroy = () => {
for (const e of effects) {
if (e.dom) remove(e.dom);
}
};
} }

@ -2,12 +2,13 @@ import { namespace_svg } from '../../../../constants.js';
import { hydrate_anchor, hydrate_nodes, hydrating } from '../hydration.js'; import { hydrate_anchor, hydrate_nodes, hydrating } from '../hydration.js';
import { empty } from '../operations.js'; import { empty } from '../operations.js';
import { import {
block,
branch,
destroy_effect, destroy_effect,
pause_effect, pause_effect,
render_effect, render_effect,
resume_effect resume_effect
} from '../../reactivity/effects.js'; } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { is_array } from '../../utils.js'; import { is_array } from '../../utils.js';
import { set_should_intro } from '../../render.js'; import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js'; import { current_each_item, set_current_each_item } from './each.js';
@ -44,6 +45,7 @@ function swap_block_dom(effect, from, to) {
export function element(anchor, get_tag, is_svg, render_fn) { export function element(anchor, get_tag, is_svg, render_fn) {
const parent_effect = /** @type {import('#client').Effect} */ (current_effect); const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
render_effect(() => {
/** @type {string | null} */ /** @type {string | null} */
let tag; let tag;
@ -63,7 +65,7 @@ export function element(anchor, get_tag, is_svg, render_fn) {
*/ */
let each_item_block = current_each_item; let each_item_block = current_each_item;
const wrapper = render_effect(() => { block(() => {
const next_tag = get_tag() || null; const next_tag = get_tag() || null;
if (next_tag === tag) return; if (next_tag === tag) return;
@ -87,7 +89,7 @@ export function element(anchor, get_tag, is_svg, render_fn) {
pause_effect(effect, () => { pause_effect(effect, () => {
effect = null; effect = null;
current_tag = null; current_tag = null;
element?.remove(); // TODO this should be unnecessary element?.remove();
}); });
} else if (next_tag === current_tag) { } else if (next_tag === current_tag) {
// same tag as is currently rendered — abort outro // same tag as is currently rendered — abort outro
@ -100,7 +102,7 @@ export function element(anchor, get_tag, is_svg, render_fn) {
} }
if (next_tag && next_tag !== current_tag) { if (next_tag && next_tag !== current_tag) {
effect = render_effect(() => { effect = branch(() => {
const prev_element = element; const prev_element = element;
element = hydrating element = hydrating
? /** @type {Element} */ (hydrate_nodes[0]) ? /** @type {Element} */ (hydrate_nodes[0])
@ -130,7 +132,7 @@ export function element(anchor, get_tag, is_svg, render_fn) {
swap_block_dom(parent_effect, prev_element, element); swap_block_dom(parent_effect, prev_element, element);
prev_element.remove(); prev_element.remove();
} }
}, true); });
} }
tag = next_tag; tag = next_tag;
@ -140,14 +142,8 @@ export function element(anchor, get_tag, is_svg, render_fn) {
set_current_each_item(previous_each_item); set_current_each_item(previous_each_item);
}); });
wrapper.ondestroy = () => { return () => {
if (element !== null) { element?.remove();
remove(element);
element = null;
}
if (effect) {
destroy_effect(effect);
}
}; };
});
} }

@ -1,7 +1,6 @@
import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js'; import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js';
import { empty } from '../operations.js'; import { empty } from '../operations.js';
import { render_effect } from '../../reactivity/effects.js'; import { block } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
/** /**
* @param {(anchor: Node) => import('#client').Dom | void} render_fn * @param {(anchor: Node) => import('#client').Dom | void} render_fn
@ -13,6 +12,9 @@ export function head(render_fn) {
let previous_hydrate_nodes = null; let previous_hydrate_nodes = null;
let was_hydrating = hydrating; let was_hydrating = hydrating;
/** @type {Comment | Text} */
var anchor;
if (hydrating) { if (hydrating) {
previous_hydrate_nodes = hydrate_nodes; previous_hydrate_nodes = hydrate_nodes;
@ -22,28 +24,12 @@ export function head(render_fn) {
} }
anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(anchor)); anchor = /** @type {import('#client').TemplateNode} */ (hydrate_anchor(anchor));
} else {
anchor = document.head.appendChild(empty());
} }
var anchor = document.head.appendChild(empty());
try { try {
/** @type {import('#client').Dom | null} */ block(() => render_fn(anchor));
var dom = null;
const head_effect = render_effect(() => {
if (dom !== null) {
remove(dom);
head_effect.dom = dom = null;
}
dom = render_fn(anchor) ?? null;
});
head_effect.ondestroy = () => {
if (dom !== null) {
remove(dom);
}
};
} finally { } finally {
if (was_hydrating) { if (was_hydrating) {
set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes)); set_hydrate_nodes(/** @type {import('#client').TemplateNode[]} */ (previous_hydrate_nodes));

@ -1,5 +1,5 @@
import { STATE_SYMBOL } from '../../../constants.js'; import { STATE_SYMBOL } from '../../../constants.js';
import { effect } from '../../../reactivity/effects.js'; import { effect, render_effect } from '../../../reactivity/effects.js';
import { untrack } from '../../../runtime.js'; import { untrack } from '../../../runtime.js';
/** /**
@ -22,13 +22,14 @@ function is_bound_this(bound_value, element_or_component) {
* @returns {void} * @returns {void}
*/ */
export function bind_this(element_or_component, update, get_value, get_parts) { export function bind_this(element_or_component, update, get_value, get_parts) {
effect(() => {
/** @type {unknown[]} */ /** @type {unknown[]} */
var old_parts; var old_parts;
/** @type {unknown[]} */ /** @type {unknown[]} */
var parts; var parts;
var e = effect(() => { render_effect(() => {
old_parts = parts; old_parts = parts;
// We only track changes to the parts, not the value itself to avoid unnecessary reruns. // We only track changes to the parts, not the value itself to avoid unnecessary reruns.
parts = get_parts?.() || []; parts = get_parts?.() || [];
@ -45,10 +46,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). return () => {
// In these cases we need to nullify the binding only if we detect that the value is still the same.
// If not, that means that another effect has now taken over the binding.
e.ondestroy = () => {
// Defer to the next tick so that all updates can be reconciled first. // 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. // This solves the case where one variable is shared across multiple this-bindings.
effect(() => { effect(() => {
@ -57,4 +55,5 @@ export function bind_this(element_or_component, update, get_value, get_parts) {
} }
}); });
}; };
});
} }

@ -16,14 +16,16 @@ import {
} from '../runtime.js'; } from '../runtime.js';
import { import {
DIRTY, DIRTY,
MANAGED, BRANCH_EFFECT,
RENDER_EFFECT, RENDER_EFFECT,
EFFECT, EFFECT,
PRE_EFFECT, PRE_EFFECT,
DESTROYED, DESTROYED,
INERT, INERT,
IS_ELSEIF, IS_ELSEIF,
EFFECT_RAN EFFECT_RAN,
BLOCK_EFFECT,
ROOT_EFFECT
} from '../constants.js'; } from '../constants.js';
import { set } from './sources.js'; import { set } from './sources.js';
import { noop } from '../../common.js'; import { noop } from '../../common.js';
@ -49,7 +51,6 @@ function create_effect(type, fn, sync, init = true) {
deriveds: null, deriveds: null,
teardown: null, teardown: null,
ctx: current_component_context, ctx: current_component_context,
ondestroy: null,
transitions: null transitions: null
}; };
@ -89,7 +90,7 @@ function create_effect(type, fn, sync, init = true) {
* @returns {boolean} * @returns {boolean}
*/ */
export function effect_active() { export function effect_active() {
return current_effect ? (current_effect.f & MANAGED) === 0 : false; return current_effect ? (current_effect.f & (BRANCH_EFFECT | ROOT_EFFECT)) === 0 : false;
} }
/** /**
@ -146,8 +147,8 @@ export function user_pre_effect(fn) {
* @param {() => void | (() => void)} fn * @param {() => void | (() => void)} fn
* @returns {() => void} * @returns {() => void}
*/ */
export function user_root_effect(fn) { export function effect_root(fn) {
const effect = render_effect(fn, true); const effect = create_effect(ROOT_EFFECT, () => untrack(fn), true);
return () => { return () => {
destroy_effect(effect); destroy_effect(effect);
}; };
@ -206,14 +207,20 @@ export function pre_effect(fn) {
/** /**
* @param {(() => void)} fn * @param {(() => void)} fn
* @param {boolean} managed
* @returns {import('#client').Effect} * @returns {import('#client').Effect}
*/ */
export function render_effect(fn, managed = false) { export function render_effect(fn) {
let flags = RENDER_EFFECT; return create_effect(RENDER_EFFECT, fn, true);
if (managed) flags |= MANAGED; }
/** @param {(() => void)} fn */
export function block(fn) {
return create_effect(RENDER_EFFECT | BLOCK_EFFECT, fn, true);
}
return create_effect(flags, fn, true); /** @param {(() => void)} fn */
export function branch(fn) {
return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true);
} }
/** /**
@ -237,16 +244,13 @@ export function destroy_effect(effect) {
remove(effect.dom); remove(effect.dom);
} }
effect.ondestroy?.();
// @ts-expect-error
effect.fn =
effect.effects = effect.effects =
effect.teardown = effect.teardown =
effect.ondestroy =
effect.ctx = effect.ctx =
effect.dom = effect.dom =
effect.deps = effect.deps =
// @ts-expect-error
effect.fn =
null; null;
} }
@ -328,7 +332,7 @@ function pause_children(effect, transitions, local) {
if (effect.effects !== null) { if (effect.effects !== null) {
for (const child of effect.effects) { for (const child of effect.effects) {
var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & MANAGED) !== 0; var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
pause_children(child, transitions, transparent ? local : false); pause_children(child, transitions, transparent ? local : false);
} }
} }
@ -359,7 +363,7 @@ function resume_children(effect, local) {
if (effect.effects !== null) { if (effect.effects !== null) {
for (const child of effect.effects) { for (const child of effect.effects) {
var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & MANAGED) !== 0; var transparent = (child.f & IS_ELSEIF) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
resume_children(child, transparent ? local : false); resume_children(child, transparent ? local : false);
} }
} }

@ -18,7 +18,7 @@ import {
untrack untrack
} from '../runtime.js'; } from '../runtime.js';
import { equals, safe_equals } from './equality.js'; import { equals, safe_equals } from './equality.js';
import { CLEAN, DERIVED, DIRTY, MANAGED, UNINITIALIZED } from '../constants.js'; import { CLEAN, DERIVED, DIRTY, BRANCH_EFFECT, UNINITIALIZED } from '../constants.js';
/** /**
* @template V * @template V
@ -131,7 +131,7 @@ export function set(signal, value) {
initialized && initialized &&
current_effect !== null && current_effect !== null &&
(current_effect.f & CLEAN) !== 0 && (current_effect.f & CLEAN) !== 0 &&
(current_effect.f & MANAGED) === 0 (current_effect.f & BRANCH_EFFECT) === 0
) { ) {
if (current_dependencies !== null && current_dependencies.includes(signal)) { if (current_dependencies !== null && current_dependencies.includes(signal)) {
set_signal_status(current_effect, DIRTY); set_signal_status(current_effect, DIRTY);

@ -40,8 +40,6 @@ export interface Effect extends Reaction {
dom: Dom | null; dom: Dom | null;
/** The associated component context */ /** The associated component context */
ctx: null | ComponentContext; ctx: null | ComponentContext;
/** Stuff to do when the effect is destroyed */
ondestroy: null | (() => void);
/** The effect function */ /** The effect function */
fn: () => void | (() => void); fn: () => void | (() => void);
/** The teardown function returned from the effect function */ /** The teardown function returned from the effect function */

@ -7,9 +7,8 @@ import {
init_operations init_operations
} from './dom/operations.js'; } from './dom/operations.js';
import { PassiveDelegatedEvents } from '../../constants.js'; import { PassiveDelegatedEvents } from '../../constants.js';
import { remove } from './dom/reconciler.js'; import { flush_sync, push, pop, current_component_context, untrack } from './runtime.js';
import { flush_sync, push, pop, current_component_context } from './runtime.js'; import { effect_root, branch } from './reactivity/effects.js';
import { render_effect, destroy_effect } from './reactivity/effects.js';
import { import {
hydrate_anchor, hydrate_anchor,
hydrate_nodes, hydrate_nodes,
@ -189,48 +188,20 @@ export function hydrate(component, options) {
* events?: { [Property in keyof Events]: (e: Events[Property]) => any }; * events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>; * context?: Map<any, any>;
* intro?: boolean; * intro?: boolean;
* recover?: false;
* }} options * }} options
* @returns {Exports} * @returns {Exports}
*/ */
function _mount(Component, options) { function _mount(
Component,
{ target, anchor, props = /** @type {Props} */ ({}), events, context, intro = false }
) {
init_operations(); init_operations();
const registered_events = new Set(); const registered_events = new Set();
const container = options.target;
should_intro = options.intro ?? false;
/** @type {Exports} */
// @ts-expect-error will be defined because the render effect runs synchronously
let component = undefined;
const effect = render_effect(() => {
if (options.context) {
push({});
/** @type {import('../client/types.js').ComponentContext} */ (current_component_context).c =
options.context;
}
if (!options.props) {
options.props = /** @type {Props} */ ({});
}
if (options.events) {
// We can't spread the object or else we'd lose the state proxy stuff, if it is one
/** @type {any} */ (options.props).$$events = options.events;
}
component =
// @ts-expect-error the public typings are not what the actual function looks like
Component(options.anchor, options.props) || {};
if (options.context) {
pop();
}
}, true);
const bound_event_listener = handle_event_propagation.bind(null, container); const bound_event_listener = handle_event_propagation.bind(null, target);
const bound_document_event_listener = handle_event_propagation.bind(null, document); const bound_document_event_listener = handle_event_propagation.bind(null, document);
should_intro = true;
/** @param {Array<string>} events */ /** @param {Array<string>} events */
const event_handle = (events) => { const event_handle = (events) => {
for (let i = 0; i < events.length; i++) { for (let i = 0; i < events.length; i++) {
@ -240,7 +211,7 @@ function _mount(Component, options) {
// Add the event listener to both the container and the document. // Add the event listener to both the container and the document.
// The container listener ensures we catch events from within in case // The container listener ensures we catch events from within in case
// the outer content stops propagation of the event. // the outer content stops propagation of the event.
container.addEventListener( target.addEventListener(
event_name, event_name,
bound_event_listener, bound_event_listener,
PassiveDelegatedEvents.includes(event_name) PassiveDelegatedEvents.includes(event_name)
@ -263,21 +234,48 @@ function _mount(Component, options) {
} }
} }
}; };
event_handle(array_from(all_registered_events)); event_handle(array_from(all_registered_events));
root_event_handles.add(event_handle); root_event_handles.add(event_handle);
mounted_components.set(component, () => { /** @type {Exports} */
// @ts-expect-error will be defined because the render effect runs synchronously
let component = undefined;
const unmount = effect_root(() => {
branch(() => {
untrack(() => {
if (context) {
push({});
var ctx = /** @type {import('#client').ComponentContext} */ (current_component_context);
ctx.c = context;
}
if (events) {
// We can't spread the object or else we'd lose the state proxy stuff, if it is one
/** @type {any} */ (props).$$events = events;
}
should_intro = intro;
// @ts-expect-error the public typings are not what the actual function looks like
component = Component(anchor, props) || {};
should_intro = true;
if (context) {
pop();
}
});
});
return () => {
for (const event_name of registered_events) { for (const event_name of registered_events) {
container.removeEventListener(event_name, bound_event_listener); target.removeEventListener(event_name, bound_event_listener);
} }
root_event_handles.delete(event_handle); root_event_handles.delete(event_handle);
const dom = effect.dom; };
if (dom !== null) {
remove(dom);
}
destroy_effect(effect);
}); });
mounted_components.set(component, unmount);
return component; return component;
} }

@ -20,8 +20,10 @@ import {
UNOWNED, UNOWNED,
DESTROYED, DESTROYED,
INERT, INERT,
MANAGED, BRANCH_EFFECT,
STATE_SYMBOL STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js'; import { add_owner } from './dev/ownership.js';
@ -359,7 +361,9 @@ export function destroy_children(signal) {
if (signal.effects) { if (signal.effects) {
for (var i = 0; i < signal.effects.length; i += 1) { for (var i = 0; i < signal.effects.length; i += 1) {
var effect = signal.effects[i]; var effect = signal.effects[i];
if ((effect.f & MANAGED) === 0) {
// TODO ideally root effects would be parentless
if ((effect.f & ROOT_EFFECT) === 0) {
destroy_effect(effect); destroy_effect(effect);
} }
} }
@ -379,7 +383,9 @@ export function destroy_children(signal) {
* @returns {void} * @returns {void}
*/ */
export function execute_effect(effect) { export function execute_effect(effect) {
if ((effect.f & DESTROYED) !== 0) { var flags = effect.f;
if ((flags & DESTROYED) !== 0) {
return; return;
} }
@ -394,7 +400,10 @@ export function execute_effect(effect) {
current_component_context = component_context; current_component_context = component_context;
try { try {
if ((flags & BLOCK_EFFECT) === 0) {
destroy_children(effect); destroy_children(effect);
}
effect.teardown?.(); effect.teardown?.();
var teardown = execute_reaction_fn(effect); var teardown = execute_reaction_fn(effect);
effect.teardown = typeof teardown === 'function' ? teardown : null; effect.teardown = typeof teardown === 'function' ? teardown : null;
@ -404,7 +413,7 @@ export function execute_effect(effect) {
} }
const parent = effect.parent; const parent = effect.parent;
if ((effect.f & PRE_EFFECT) !== 0 && parent !== null) { if ((flags & PRE_EFFECT) !== 0 && parent !== null) {
flush_local_pre_effects(parent); flush_local_pre_effects(parent);
} }
} }
@ -488,7 +497,7 @@ export function schedule_effect(signal) {
// before this effect is scheduled. We know they will be destroyed // 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 // so we can make them inert to avoid having to find them in the
// queue and remove them. // queue and remove them.
if ((flags & MANAGED) === 0) { if ((flags & BRANCH_EFFECT) === 0) {
mark_subtree_children_inert(signal, true); mark_subtree_children_inert(signal, true);
} }
} else { } else {
@ -709,7 +718,11 @@ export function get(signal) {
} }
// Register the dependency on the current reaction signal. // Register the dependency on the current reaction signal.
if (current_reaction !== null && (current_reaction.f & MANAGED) === 0 && !current_untracking) { if (
current_reaction !== null &&
(current_reaction.f & BRANCH_EFFECT) === 0 &&
!current_untracking
) {
const unowned = (current_reaction.f & UNOWNED) !== 0; const unowned = (current_reaction.f & UNOWNED) !== 0;
const dependencies = current_reaction.deps; const dependencies = current_reaction.deps;
if ( if (
@ -734,7 +747,7 @@ export function get(signal) {
current_untracked_writes !== null && current_untracked_writes !== null &&
current_effect !== null && current_effect !== null &&
(current_effect.f & CLEAN) !== 0 && (current_effect.f & CLEAN) !== 0 &&
(current_effect.f & MANAGED) === 0 && (current_effect.f & BRANCH_EFFECT) === 0 &&
current_untracked_writes.includes(signal) current_untracked_writes.includes(signal)
) { ) {
set_signal_status(current_effect, DIRTY); set_signal_status(current_effect, DIRTY);

@ -1,4 +1,4 @@
import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js'; import { pre_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../main/main-client.js'; import { flushSync } from '../main/main-client.js';
import { ReactiveMap } from './map.js'; import { ReactiveMap } from './map.js';
import { assert, test } from 'vitest'; import { assert, test } from 'vitest';
@ -14,7 +14,7 @@ test('map.values()', () => {
const log: any = []; const log: any = [];
const cleanup = user_root_effect(() => { const cleanup = effect_root(() => {
pre_effect(() => { pre_effect(() => {
log.push(map.size); log.push(map.size);
}); });
@ -50,7 +50,7 @@ test('map.get(...)', () => {
const log: any = []; const log: any = [];
const cleanup = user_root_effect(() => { const cleanup = effect_root(() => {
pre_effect(() => { pre_effect(() => {
log.push('get 1', map.get(1)); log.push('get 1', map.get(1));
}); });
@ -86,7 +86,7 @@ test('map.has(...)', () => {
const log: any = []; const log: any = [];
const cleanup = user_root_effect(() => { const cleanup = effect_root(() => {
pre_effect(() => { pre_effect(() => {
log.push('has 1', map.has(1)); log.push('has 1', map.has(1));
}); });
@ -129,7 +129,7 @@ test('map handling of undefined values', () => {
const log: any = []; const log: any = [];
const cleanup = user_root_effect(() => { const cleanup = effect_root(() => {
map.set(1, undefined); map.set(1, undefined);
pre_effect(() => { pre_effect(() => {

@ -1,4 +1,4 @@
import { pre_effect, user_root_effect } from '../internal/client/reactivity/effects.js'; import { pre_effect, effect_root } from '../internal/client/reactivity/effects.js';
import { flushSync } from '../main/main-client.js'; import { flushSync } from '../main/main-client.js';
import { ReactiveSet } from './set.js'; import { ReactiveSet } from './set.js';
import { assert, test } from 'vitest'; import { assert, test } from 'vitest';
@ -8,7 +8,7 @@ test('set.values()', () => {
const log: any = []; const log: any = [];
const cleanup = user_root_effect(() => { const cleanup = effect_root(() => {
pre_effect(() => { pre_effect(() => {
log.push(set.size); log.push(set.size);
}); });
@ -40,7 +40,7 @@ test('set.has(...)', () => {
const log: any = []; const log: any = [];
const cleanup = user_root_effect(() => { const cleanup = effect_root(() => {
pre_effect(() => { pre_effect(() => {
log.push('has 1', set.has(1)); log.push('has 1', set.has(1));
}); });

@ -24,7 +24,7 @@ function run_test(runes: boolean, fn: (runes: boolean) => () => void) {
let execute: any; let execute: any;
const signal = render_effect(() => { const signal = render_effect(() => {
execute = fn(runes); execute = fn(runes);
}, true); });
$.pop(); $.pop();
execute(); execute();
destroy_effect(signal); destroy_effect(signal);

Loading…
Cancel
Save