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[]} */ (
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') {

@ -2,7 +2,8 @@ export const DERIVED = 1 << 1;
export const EFFECT = 1 << 2;
export const PRE_EFFECT = 1 << 3;
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 CLEAN = 1 << 8;
export const DIRTY = 1 << 9;
@ -11,6 +12,7 @@ export const INERT = 1 << 11;
export const DESTROYED = 1 << 12;
export const IS_ELSEIF = 1 << 13;
export const EFFECT_RAN = 1 << 14;
export const ROOT_EFFECT = 1 << 15;
export const UNINITIALIZED = Symbol();
export const STATE_SYMBOL = Symbol('$state');

@ -1,5 +1,4 @@
import { is_promise } from '../../../common.js';
import { remove } from '../reconciler.js';
import {
current_component_context,
flushSync,
@ -7,7 +6,7 @@ import {
set_current_effect,
set_current_reaction
} 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';
/**
@ -39,10 +38,10 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
* @param {any} value
*/
function create_effect(fn, value) {
set_current_effect(branch);
set_current_reaction(branch); // TODO do we need both?
set_current_effect(effect);
set_current_reaction(effect); // TODO do we need both?
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_reaction(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)
flushSync();
return effect;
return e;
}
const branch = render_effect(() => {
const effect = block(() => {
if (input === (input = get_input())) return;
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_effect && (pending_effect.f & INERT) === 0) {
if (pending_effect.dom) remove(pending_effect.dom);
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);
@ -96,19 +94,11 @@ export function await_block(anchor, get_input, pending_fn, then_fn, catch_fn) {
if (then_fn) {
if (then_effect) {
if (then_effect.dom) remove(then_effect.dom);
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,26 +39,28 @@ export function css_props(anchor, is_html, props, component) {
component(component_anchor);
/** @type {Record<string, string>} */
let current_props = {};
render_effect(() => {
/** @type {Record<string, string>} */
let current_props = {};
const effect = render_effect(() => {
const next_props = props();
render_effect(() => {
const next_props = props();
for (const key in current_props) {
if (!(key in next_props)) {
element.style.removeProperty(key);
for (const key in current_props) {
if (!(key in next_props)) {
element.style.removeProperty(key);
}
}
}
for (const key in next_props) {
element.style.setProperty(key, next_props[key]);
}
for (const key in next_props) {
element.style.setProperty(key, next_props[key]);
}
current_props = next_props;
});
current_props = next_props;
});
effect.ondestroy = () => {
remove(element);
};
return () => {
remove(element);
};
});
}

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

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

@ -1,12 +1,7 @@
import { IS_ELSEIF } from '../../constants.js';
import { hydrate_nodes, hydrating, set_hydrating } from '../hydration.js';
import { remove } from '../reconciler.js';
import {
destroy_effect,
pause_effect,
render_effect,
resume_effect
} from '../../reactivity/effects.js';
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
/**
* @param {Comment} anchor
@ -32,7 +27,7 @@ export function if_block(
/** @type {boolean | null} */
let condition = null;
const if_effect = render_effect(() => {
const effect = block(() => {
if (condition === (condition = !!get_condition())) return;
/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
@ -61,7 +56,7 @@ export function if_block(
if (consequent_effect) {
resume_effect(consequent_effect);
} else {
consequent_effect = render_effect(() => consequent_fn(anchor), true);
consequent_effect = branch(() => consequent_fn(anchor));
}
if (alternate_effect) {
@ -73,7 +68,7 @@ export function if_block(
if (alternate_effect) {
resume_effect(alternate_effect);
} else if (alternate_fn) {
alternate_effect = render_effect(() => alternate_fn(anchor), true);
alternate_effect = branch(() => alternate_fn(anchor));
}
if (consequent_effect) {
@ -90,17 +85,6 @@ export function if_block(
});
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 { remove } from '../reconciler.js';
import { pause_effect, render_effect } from '../../reactivity/effects.js';
import { block, branch, pause_effect } from '../../reactivity/effects.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} */
let effect;
/**
* 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(() => {
block(() => {
if (safe_not_equal(key, (key = get_key()))) {
if (effect) {
var e = effect;
pause_effect(e, () => {
effects.delete(e);
});
pause_effect(effect);
}
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,
// not when an eagerly-read prop inside the snippet function changes
var dom = untrack(() => /** @type {SnippetFn} */ (snippet_fn)(node, ...args));
}
return () => {
if (dom !== undefined) {
remove(dom);
return () => remove(dom);
}
};
}
});
}

@ -1,8 +1,6 @@
import { pause_effect, render_effect } from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { current_effect } from '../../runtime.js';
import { block, branch, pause_effect } from '../../reactivity/effects.js';
// TODO this is very similar to `key`, can we deduplicate?
// TODO seems weird that `anchor` is unused here — possible bug?
/**
* @template P
@ -16,46 +14,19 @@ export function component(anchor, get_component, render_fn) {
/** @type {C} */
let component;
/** @type {import('#client').Effect} */
/** @type {import('#client').Effect | null} */
let effect;
/**
* 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(() => {
block(() => {
if (component === (component = get_component())) return;
if (effect) {
var e = effect;
pause_effect(e, () => {
effects.delete(e);
});
pause_effect(effect);
effect = null;
}
if (component) {
effect = render_effect(() => {
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);
effect = branch(() => render_fn(component));
}
});
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 { empty } from '../operations.js';
import {
block,
branch,
destroy_effect,
pause_effect,
render_effect,
resume_effect
} from '../../reactivity/effects.js';
import { remove } from '../reconciler.js';
import { is_array } from '../../utils.js';
import { set_should_intro } from '../../render.js';
import { current_each_item, set_current_each_item } from './each.js';
@ -44,110 +45,105 @@ function swap_block_dom(effect, from, to) {
export function element(anchor, get_tag, is_svg, render_fn) {
const parent_effect = /** @type {import('#client').Effect} */ (current_effect);
/** @type {string | null} */
let tag;
/** @type {string | null} */
let current_tag;
/** @type {null | Element} */
let element = null;
/** @type {import('#client').Effect | null} */
let effect;
/**
* The keyed `{#each ...}` item block, if any, that this element is inside.
* We track this so we can set it when changing the element, allowing any
* `animate:` directive to bind itself to the correct block
*/
let each_item_block = current_each_item;
const wrapper = render_effect(() => {
const next_tag = get_tag() || null;
if (next_tag === tag) return;
// See explanation of `each_item_block` above
var previous_each_item = current_each_item;
set_current_each_item(each_item_block);
// We try our best infering the namespace in case it's not possible to determine statically,
// but on the first render on the client (without hydration) the parent will be undefined,
// since the anchor is not attached to its parent / the dom yet.
const ns =
is_svg || next_tag === 'svg'
? namespace_svg
: is_svg === false || anchor.parentElement?.tagName === 'foreignObject'
? null
: anchor.parentElement?.namespaceURI ?? null;
if (effect) {
if (next_tag === null) {
// start outro
pause_effect(effect, () => {
effect = null;
current_tag = null;
element?.remove(); // TODO this should be unnecessary
});
} else if (next_tag === current_tag) {
// same tag as is currently rendered — abort outro
resume_effect(effect);
} else {
// tag is changing — destroy immediately, render contents without intro transitions
destroy_effect(effect);
set_should_intro(false);
render_effect(() => {
/** @type {string | null} */
let tag;
/** @type {string | null} */
let current_tag;
/** @type {null | Element} */
let element = null;
/** @type {import('#client').Effect | null} */
let effect;
/**
* The keyed `{#each ...}` item block, if any, that this element is inside.
* We track this so we can set it when changing the element, allowing any
* `animate:` directive to bind itself to the correct block
*/
let each_item_block = current_each_item;
block(() => {
const next_tag = get_tag() || null;
if (next_tag === tag) return;
// See explanation of `each_item_block` above
var previous_each_item = current_each_item;
set_current_each_item(each_item_block);
// We try our best infering the namespace in case it's not possible to determine statically,
// but on the first render on the client (without hydration) the parent will be undefined,
// since the anchor is not attached to its parent / the dom yet.
const ns =
is_svg || next_tag === 'svg'
? namespace_svg
: is_svg === false || anchor.parentElement?.tagName === 'foreignObject'
? null
: anchor.parentElement?.namespaceURI ?? null;
if (effect) {
if (next_tag === null) {
// start outro
pause_effect(effect, () => {
effect = null;
current_tag = null;
element?.remove();
});
} else if (next_tag === current_tag) {
// same tag as is currently rendered — abort outro
resume_effect(effect);
} else {
// tag is changing — destroy immediately, render contents without intro transitions
destroy_effect(effect);
set_should_intro(false);
}
}
}
if (next_tag && next_tag !== current_tag) {
effect = render_effect(() => {
const prev_element = element;
element = hydrating
? /** @type {Element} */ (hydrate_nodes[0])
: ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
if (render_fn) {
// If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly
var child_anchor = hydrating
? element.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild))
: element.appendChild(empty());
if (child_anchor) {
// `child_anchor` can be undefined if this is a void element with children,
// i.e. `<svelte:element this={"hr"}>...</svelte:element>`. This is
// user error, but we warn on it elsewhere (in dev) so here we just
// silently ignore it
render_fn(element, child_anchor);
if (next_tag && next_tag !== current_tag) {
effect = branch(() => {
const prev_element = element;
element = hydrating
? /** @type {Element} */ (hydrate_nodes[0])
: ns
? document.createElementNS(ns, next_tag)
: document.createElement(next_tag);
if (render_fn) {
// If hydrating, use the existing ssr comment as the anchor so that the
// inner open and close methods can pick up the existing nodes correctly
var child_anchor = hydrating
? element.firstChild && hydrate_anchor(/** @type {Comment} */ (element.firstChild))
: element.appendChild(empty());
if (child_anchor) {
// `child_anchor` can be undefined if this is a void element with children,
// i.e. `<svelte:element this={"hr"}>...</svelte:element>`. This is
// user error, but we warn on it elsewhere (in dev) so here we just
// silently ignore it
render_fn(element, child_anchor);
}
}
}
anchor.before(element);
if (prev_element) {
swap_block_dom(parent_effect, prev_element, element);
prev_element.remove();
}
}, true);
}
anchor.before(element);
tag = next_tag;
if (tag) current_tag = tag;
set_should_intro(true);
if (prev_element) {
swap_block_dom(parent_effect, prev_element, element);
prev_element.remove();
}
});
}
set_current_each_item(previous_each_item);
});
tag = next_tag;
if (tag) current_tag = tag;
set_should_intro(true);
wrapper.ondestroy = () => {
if (element !== null) {
remove(element);
element = null;
}
set_current_each_item(previous_each_item);
});
if (effect) {
destroy_effect(effect);
}
};
return () => {
element?.remove();
};
});
}

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

@ -1,5 +1,5 @@
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';
/**
@ -22,39 +22,38 @@ function is_bound_this(bound_value, element_or_component) {
* @returns {void}
*/
export function bind_this(element_or_component, update, get_value, get_parts) {
/** @type {unknown[]} */
var old_parts;
effect(() => {
/** @type {unknown[]} */
var old_parts;
/** @type {unknown[]} */
var parts;
/** @type {unknown[]} */
var parts;
var e = effect(() => {
old_parts = parts;
// We only track changes to the parts, not the value itself to avoid unnecessary reruns.
parts = get_parts?.() || [];
render_effect(() => {
old_parts = parts;
// We only track changes to the parts, not the value itself to avoid unnecessary reruns.
parts = get_parts?.() || [];
untrack(() => {
if (element_or_component !== get_value(...parts)) {
update(element_or_component, ...parts);
// If this is an effect rerun (cause: each block context changes), then nullfiy the binding at
// the previous position if it isn't already taken over by a different effect.
if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) {
update(null, ...old_parts);
untrack(() => {
if (element_or_component !== get_value(...parts)) {
update(element_or_component, ...parts);
// If this is an effect rerun (cause: each block context changes), then nullfiy the binding at
// the previous position if it isn't already taken over by a different effect.
if (old_parts && is_bound_this(get_value(...old_parts), element_or_component)) {
update(null, ...old_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.
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(() => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts);
}
});
};
return () => {
// 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(() => {
if (parts && is_bound_this(get_value(...parts), element_or_component)) {
update(null, ...parts);
}
});
};
});
}

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

@ -18,7 +18,7 @@ import {
untrack
} from '../runtime.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
@ -131,7 +131,7 @@ export function set(signal, value) {
initialized &&
current_effect !== null &&
(current_effect.f & CLEAN) !== 0 &&
(current_effect.f & MANAGED) === 0
(current_effect.f & BRANCH_EFFECT) === 0
) {
if (current_dependencies !== null && current_dependencies.includes(signal)) {
set_signal_status(current_effect, DIRTY);

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

@ -7,9 +7,8 @@ import {
init_operations
} from './dom/operations.js';
import { PassiveDelegatedEvents } from '../../constants.js';
import { remove } from './dom/reconciler.js';
import { flush_sync, push, pop, current_component_context } from './runtime.js';
import { render_effect, destroy_effect } from './reactivity/effects.js';
import { flush_sync, push, pop, current_component_context, untrack } from './runtime.js';
import { effect_root, branch } from './reactivity/effects.js';
import {
hydrate_anchor,
hydrate_nodes,
@ -189,48 +188,20 @@ export function hydrate(component, options) {
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>;
* intro?: boolean;
* recover?: false;
* }} options
* @returns {Exports}
*/
function _mount(Component, options) {
function _mount(
Component,
{ target, anchor, props = /** @type {Props} */ ({}), events, context, intro = false }
) {
init_operations();
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);
should_intro = true;
/** @param {Array<string>} events */
const event_handle = (events) => {
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.
// The container listener ensures we catch events from within in case
// the outer content stops propagation of the event.
container.addEventListener(
target.addEventListener(
event_name,
bound_event_listener,
PassiveDelegatedEvents.includes(event_name)
@ -263,21 +234,48 @@ function _mount(Component, options) {
}
}
};
event_handle(array_from(all_registered_events));
root_event_handles.add(event_handle);
mounted_components.set(component, () => {
for (const event_name of registered_events) {
container.removeEventListener(event_name, bound_event_listener);
}
root_event_handles.delete(event_handle);
const dom = effect.dom;
if (dom !== null) {
remove(dom);
}
destroy_effect(effect);
/** @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) {
target.removeEventListener(event_name, bound_event_listener);
}
root_event_handles.delete(event_handle);
};
});
mounted_components.set(component, unmount);
return component;
}

@ -20,8 +20,10 @@ import {
UNOWNED,
DESTROYED,
INERT,
MANAGED,
STATE_SYMBOL
BRANCH_EFFECT,
STATE_SYMBOL,
BLOCK_EFFECT,
ROOT_EFFECT
} from './constants.js';
import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js';
@ -359,7 +361,9 @@ export function destroy_children(signal) {
if (signal.effects) {
for (var i = 0; i < signal.effects.length; i += 1) {
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);
}
}
@ -379,7 +383,9 @@ export function destroy_children(signal) {
* @returns {void}
*/
export function execute_effect(effect) {
if ((effect.f & DESTROYED) !== 0) {
var flags = effect.f;
if ((flags & DESTROYED) !== 0) {
return;
}
@ -394,7 +400,10 @@ export function execute_effect(effect) {
current_component_context = component_context;
try {
destroy_children(effect);
if ((flags & BLOCK_EFFECT) === 0) {
destroy_children(effect);
}
effect.teardown?.();
var teardown = execute_reaction_fn(effect);
effect.teardown = typeof teardown === 'function' ? teardown : null;
@ -404,7 +413,7 @@ export function execute_effect(effect) {
}
const parent = effect.parent;
if ((effect.f & PRE_EFFECT) !== 0 && parent !== null) {
if ((flags & PRE_EFFECT) !== 0 && parent !== null) {
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
// so we can make them inert to avoid having to find them in the
// queue and remove them.
if ((flags & MANAGED) === 0) {
if ((flags & BRANCH_EFFECT) === 0) {
mark_subtree_children_inert(signal, true);
}
} else {
@ -709,7 +718,11 @@ export function get(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 dependencies = current_reaction.deps;
if (
@ -734,7 +747,7 @@ export function get(signal) {
current_untracked_writes !== null &&
current_effect !== null &&
(current_effect.f & CLEAN) !== 0 &&
(current_effect.f & MANAGED) === 0 &&
(current_effect.f & BRANCH_EFFECT) === 0 &&
current_untracked_writes.includes(signal)
) {
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 { ReactiveMap } from './map.js';
import { assert, test } from 'vitest';
@ -14,7 +14,7 @@ test('map.values()', () => {
const log: any = [];
const cleanup = user_root_effect(() => {
const cleanup = effect_root(() => {
pre_effect(() => {
log.push(map.size);
});
@ -50,7 +50,7 @@ test('map.get(...)', () => {
const log: any = [];
const cleanup = user_root_effect(() => {
const cleanup = effect_root(() => {
pre_effect(() => {
log.push('get 1', map.get(1));
});
@ -86,7 +86,7 @@ test('map.has(...)', () => {
const log: any = [];
const cleanup = user_root_effect(() => {
const cleanup = effect_root(() => {
pre_effect(() => {
log.push('has 1', map.has(1));
});
@ -129,7 +129,7 @@ test('map handling of undefined values', () => {
const log: any = [];
const cleanup = user_root_effect(() => {
const cleanup = effect_root(() => {
map.set(1, undefined);
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 { ReactiveSet } from './set.js';
import { assert, test } from 'vitest';
@ -8,7 +8,7 @@ test('set.values()', () => {
const log: any = [];
const cleanup = user_root_effect(() => {
const cleanup = effect_root(() => {
pre_effect(() => {
log.push(set.size);
});
@ -40,7 +40,7 @@ test('set.has(...)', () => {
const log: any = [];
const cleanup = user_root_effect(() => {
const cleanup = effect_root(() => {
pre_effect(() => {
log.push('has 1', set.has(1));
});

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

Loading…
Cancel
Save