mirror of https://github.com/sveltejs/svelte
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
645 lines
15 KiB
645 lines
15 KiB
/** @import { ComponentContext, ComponentContextLegacy, Derived, Effect, TemplateNode, TransitionManager } from '#client' */
|
|
import {
|
|
check_dirtiness,
|
|
active_effect,
|
|
active_reaction,
|
|
update_effect,
|
|
get,
|
|
is_destroying_effect,
|
|
is_flushing_effect,
|
|
remove_reactions,
|
|
schedule_effect,
|
|
set_active_reaction,
|
|
set_is_destroying_effect,
|
|
set_is_flushing_effect,
|
|
set_signal_status,
|
|
untrack,
|
|
skip_reaction,
|
|
untracking
|
|
} from '../runtime.js';
|
|
import {
|
|
DIRTY,
|
|
BRANCH_EFFECT,
|
|
RENDER_EFFECT,
|
|
EFFECT,
|
|
DESTROYED,
|
|
INERT,
|
|
EFFECT_RAN,
|
|
BLOCK_EFFECT,
|
|
ROOT_EFFECT,
|
|
EFFECT_TRANSPARENT,
|
|
DERIVED,
|
|
UNOWNED,
|
|
CLEAN,
|
|
INSPECT_EFFECT,
|
|
HEAD_EFFECT,
|
|
MAYBE_DIRTY,
|
|
EFFECT_HAS_DERIVED,
|
|
BOUNDARY_EFFECT
|
|
} from '../constants.js';
|
|
import { set } from './sources.js';
|
|
import * as e from '../errors.js';
|
|
import { DEV } from 'esm-env';
|
|
import { define_property } from '../../shared/utils.js';
|
|
import { get_next_sibling } from '../dom/operations.js';
|
|
import { derived, destroy_derived } from './deriveds.js';
|
|
import { component_context, dev_current_component_function } from '../context.js';
|
|
|
|
/**
|
|
* @param {'$effect' | '$effect.pre' | '$inspect'} rune
|
|
*/
|
|
export function validate_effect(rune) {
|
|
if (active_effect === null && active_reaction === null) {
|
|
e.effect_orphan(rune);
|
|
}
|
|
|
|
if (active_reaction !== null && (active_reaction.f & UNOWNED) !== 0) {
|
|
e.effect_in_unowned_derived();
|
|
}
|
|
|
|
if (is_destroying_effect) {
|
|
e.effect_in_teardown(rune);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Effect} effect
|
|
* @param {Effect} parent_effect
|
|
*/
|
|
function push_effect(effect, parent_effect) {
|
|
var parent_last = parent_effect.last;
|
|
if (parent_last === null) {
|
|
parent_effect.last = parent_effect.first = effect;
|
|
} else {
|
|
parent_last.next = effect;
|
|
effect.prev = parent_last;
|
|
parent_effect.last = effect;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {number} type
|
|
* @param {null | (() => void | (() => void))} fn
|
|
* @param {boolean} sync
|
|
* @param {boolean} push
|
|
* @returns {Effect}
|
|
*/
|
|
function create_effect(type, fn, sync, push = true) {
|
|
var is_root = (type & ROOT_EFFECT) !== 0;
|
|
var parent_effect = active_effect;
|
|
|
|
if (DEV) {
|
|
// Ensure the parent is never an inspect effect
|
|
while (parent_effect !== null && (parent_effect.f & INSPECT_EFFECT) !== 0) {
|
|
parent_effect = parent_effect.parent;
|
|
}
|
|
}
|
|
|
|
/** @type {Effect} */
|
|
var effect = {
|
|
ctx: component_context,
|
|
deps: null,
|
|
deriveds: null,
|
|
nodes_start: null,
|
|
nodes_end: null,
|
|
f: type | DIRTY,
|
|
first: null,
|
|
fn,
|
|
last: null,
|
|
next: null,
|
|
parent: is_root ? null : parent_effect,
|
|
prev: null,
|
|
teardown: null,
|
|
transitions: null,
|
|
wv: 0
|
|
};
|
|
|
|
if (DEV) {
|
|
effect.component_function = dev_current_component_function;
|
|
}
|
|
|
|
if (sync) {
|
|
var previously_flushing_effect = is_flushing_effect;
|
|
|
|
try {
|
|
set_is_flushing_effect(true);
|
|
update_effect(effect);
|
|
effect.f |= EFFECT_RAN;
|
|
} catch (e) {
|
|
destroy_effect(effect);
|
|
throw e;
|
|
} finally {
|
|
set_is_flushing_effect(previously_flushing_effect);
|
|
}
|
|
} else if (fn !== null) {
|
|
schedule_effect(effect);
|
|
}
|
|
|
|
// if an effect has no dependencies, no DOM and no teardown function,
|
|
// don't bother adding it to the effect tree
|
|
var inert =
|
|
sync &&
|
|
effect.deps === null &&
|
|
effect.first === null &&
|
|
effect.nodes_start === null &&
|
|
effect.teardown === null &&
|
|
(effect.f & (EFFECT_HAS_DERIVED | BOUNDARY_EFFECT)) === 0;
|
|
|
|
if (!inert && !is_root && push) {
|
|
if (parent_effect !== null) {
|
|
push_effect(effect, parent_effect);
|
|
}
|
|
|
|
// if we're in a derived, add the effect there too
|
|
if (active_reaction !== null && (active_reaction.f & DERIVED) !== 0) {
|
|
var derived = /** @type {Derived} */ (active_reaction);
|
|
(derived.children ??= []).push(effect);
|
|
}
|
|
}
|
|
|
|
return effect;
|
|
}
|
|
|
|
/**
|
|
* Internal representation of `$effect.tracking()`
|
|
* @returns {boolean}
|
|
*/
|
|
export function effect_tracking() {
|
|
if (active_reaction === null || untracking) {
|
|
return false;
|
|
}
|
|
|
|
// If it's skipped, that's because we're inside an unowned
|
|
// that is not being tracked by another reaction
|
|
return !skip_reaction;
|
|
}
|
|
|
|
/**
|
|
* @param {() => void} fn
|
|
*/
|
|
export function teardown(fn) {
|
|
const effect = create_effect(RENDER_EFFECT, null, false);
|
|
set_signal_status(effect, CLEAN);
|
|
effect.teardown = fn;
|
|
return effect;
|
|
}
|
|
|
|
/**
|
|
* Internal representation of `$effect(...)`
|
|
* @param {() => void | (() => void)} fn
|
|
*/
|
|
export function user_effect(fn) {
|
|
validate_effect('$effect');
|
|
|
|
// Non-nested `$effect(...)` in a component should be deferred
|
|
// until the component is mounted
|
|
var defer =
|
|
active_effect !== null &&
|
|
(active_effect.f & BRANCH_EFFECT) !== 0 &&
|
|
component_context !== null &&
|
|
!component_context.m;
|
|
|
|
if (DEV) {
|
|
define_property(fn, 'name', {
|
|
value: '$effect'
|
|
});
|
|
}
|
|
|
|
if (defer) {
|
|
var context = /** @type {ComponentContext} */ (component_context);
|
|
(context.e ??= []).push({
|
|
fn,
|
|
effect: active_effect,
|
|
reaction: active_reaction
|
|
});
|
|
} else {
|
|
var signal = effect(fn);
|
|
return signal;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal representation of `$effect.pre(...)`
|
|
* @param {() => void | (() => void)} fn
|
|
* @returns {Effect}
|
|
*/
|
|
export function user_pre_effect(fn) {
|
|
validate_effect('$effect.pre');
|
|
if (DEV) {
|
|
define_property(fn, 'name', {
|
|
value: '$effect.pre'
|
|
});
|
|
}
|
|
return render_effect(fn);
|
|
}
|
|
|
|
/** @param {() => void | (() => void)} fn */
|
|
export function inspect_effect(fn) {
|
|
return create_effect(INSPECT_EFFECT, fn, true);
|
|
}
|
|
|
|
/**
|
|
* Internal representation of `$effect.root(...)`
|
|
* @param {() => void | (() => void)} fn
|
|
* @returns {() => void}
|
|
*/
|
|
export function effect_root(fn) {
|
|
const effect = create_effect(ROOT_EFFECT, fn, true);
|
|
|
|
return () => {
|
|
destroy_effect(effect);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* An effect root whose children can transition out
|
|
* @param {() => void} fn
|
|
* @returns {(options?: { outro?: boolean }) => Promise<void>}
|
|
*/
|
|
export function component_root(fn) {
|
|
const effect = create_effect(ROOT_EFFECT, fn, true);
|
|
|
|
return (options = {}) => {
|
|
return new Promise((fulfil) => {
|
|
if (options.outro) {
|
|
pause_effect(effect, () => {
|
|
destroy_effect(effect);
|
|
fulfil(undefined);
|
|
});
|
|
} else {
|
|
destroy_effect(effect);
|
|
fulfil(undefined);
|
|
}
|
|
});
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {() => void | (() => void)} fn
|
|
* @returns {Effect}
|
|
*/
|
|
export function effect(fn) {
|
|
return create_effect(EFFECT, fn, false);
|
|
}
|
|
|
|
/**
|
|
* Internal representation of `$: ..`
|
|
* @param {() => any} deps
|
|
* @param {() => void | (() => void)} fn
|
|
*/
|
|
export function legacy_pre_effect(deps, fn) {
|
|
var context = /** @type {ComponentContextLegacy} */ (component_context);
|
|
|
|
/** @type {{ effect: null | Effect, ran: boolean }} */
|
|
var token = { effect: null, ran: false };
|
|
context.l.r1.push(token);
|
|
|
|
token.effect = render_effect(() => {
|
|
deps();
|
|
|
|
// If this legacy pre effect has already run before the end of the reset, then
|
|
// bail out to emulate the same behavior.
|
|
if (token.ran) return;
|
|
|
|
token.ran = true;
|
|
set(context.l.r2, true);
|
|
untrack(fn);
|
|
});
|
|
}
|
|
|
|
export function legacy_pre_effect_reset() {
|
|
var context = /** @type {ComponentContextLegacy} */ (component_context);
|
|
|
|
render_effect(() => {
|
|
if (!get(context.l.r2)) return;
|
|
|
|
// Run dirty `$:` statements
|
|
for (var token of context.l.r1) {
|
|
var effect = token.effect;
|
|
|
|
// If the effect is CLEAN, then make it MAYBE_DIRTY. This ensures we traverse through
|
|
// the effects dependencies and correctly ensure each dependency is up-to-date.
|
|
if ((effect.f & CLEAN) !== 0) {
|
|
set_signal_status(effect, MAYBE_DIRTY);
|
|
}
|
|
|
|
if (check_dirtiness(effect)) {
|
|
update_effect(effect);
|
|
}
|
|
|
|
token.ran = false;
|
|
}
|
|
|
|
context.l.r2.v = false; // set directly to avoid rerunning this effect
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {() => void | (() => void)} fn
|
|
* @returns {Effect}
|
|
*/
|
|
export function render_effect(fn) {
|
|
return create_effect(RENDER_EFFECT, fn, true);
|
|
}
|
|
|
|
/**
|
|
* @param {(...expressions: any) => void | (() => void)} fn
|
|
* @param {Array<() => any>} thunks
|
|
* @returns {Effect}
|
|
*/
|
|
export function template_effect(fn, thunks = [], d = derived) {
|
|
const deriveds = thunks.map(d);
|
|
const effect = () => fn(...deriveds.map(get));
|
|
|
|
if (DEV) {
|
|
define_property(effect, 'name', {
|
|
value: '{expression}'
|
|
});
|
|
}
|
|
|
|
return block(effect);
|
|
}
|
|
|
|
/**
|
|
* @param {(() => void)} fn
|
|
* @param {number} flags
|
|
*/
|
|
export function block(fn, flags = 0) {
|
|
return create_effect(RENDER_EFFECT | BLOCK_EFFECT | flags, fn, true);
|
|
}
|
|
|
|
/**
|
|
* @param {(() => void)} fn
|
|
* @param {boolean} [push]
|
|
*/
|
|
export function branch(fn, push = true) {
|
|
return create_effect(RENDER_EFFECT | BRANCH_EFFECT, fn, true, push);
|
|
}
|
|
|
|
/**
|
|
* @param {Effect} effect
|
|
*/
|
|
export function execute_effect_teardown(effect) {
|
|
var teardown = effect.teardown;
|
|
if (teardown !== null) {
|
|
const previously_destroying_effect = is_destroying_effect;
|
|
const previous_reaction = active_reaction;
|
|
set_is_destroying_effect(true);
|
|
set_active_reaction(null);
|
|
try {
|
|
teardown.call(null);
|
|
} finally {
|
|
set_is_destroying_effect(previously_destroying_effect);
|
|
set_active_reaction(previous_reaction);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Effect} signal
|
|
* @returns {void}
|
|
*/
|
|
export function destroy_effect_deriveds(signal) {
|
|
var deriveds = signal.deriveds;
|
|
|
|
if (deriveds !== null) {
|
|
signal.deriveds = null;
|
|
|
|
for (var i = 0; i < deriveds.length; i += 1) {
|
|
destroy_derived(deriveds[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Effect} signal
|
|
* @param {boolean} remove_dom
|
|
* @returns {void}
|
|
*/
|
|
export function destroy_effect_children(signal, remove_dom = false) {
|
|
var effect = signal.first;
|
|
signal.first = signal.last = null;
|
|
|
|
while (effect !== null) {
|
|
var next = effect.next;
|
|
destroy_effect(effect, remove_dom);
|
|
effect = next;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Effect} signal
|
|
* @returns {void}
|
|
*/
|
|
export function destroy_block_effect_children(signal) {
|
|
var effect = signal.first;
|
|
|
|
while (effect !== null) {
|
|
var next = effect.next;
|
|
if ((effect.f & BRANCH_EFFECT) === 0) {
|
|
destroy_effect(effect);
|
|
}
|
|
effect = next;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Effect} effect
|
|
* @param {boolean} [remove_dom]
|
|
* @returns {void}
|
|
*/
|
|
export function destroy_effect(effect, remove_dom = true) {
|
|
var removed = false;
|
|
|
|
if ((remove_dom || (effect.f & HEAD_EFFECT) !== 0) && effect.nodes_start !== null) {
|
|
/** @type {TemplateNode | null} */
|
|
var node = effect.nodes_start;
|
|
var end = effect.nodes_end;
|
|
|
|
while (node !== null) {
|
|
/** @type {TemplateNode | null} */
|
|
var next = node === end ? null : /** @type {TemplateNode} */ (get_next_sibling(node));
|
|
|
|
node.remove();
|
|
node = next;
|
|
}
|
|
|
|
removed = true;
|
|
}
|
|
|
|
destroy_effect_children(effect, remove_dom && !removed);
|
|
destroy_effect_deriveds(effect);
|
|
remove_reactions(effect, 0);
|
|
set_signal_status(effect, DESTROYED);
|
|
|
|
var transitions = effect.transitions;
|
|
|
|
if (transitions !== null) {
|
|
for (const transition of transitions) {
|
|
transition.stop();
|
|
}
|
|
}
|
|
|
|
execute_effect_teardown(effect);
|
|
|
|
var parent = effect.parent;
|
|
|
|
// If the parent doesn't have any children, then skip this work altogether
|
|
if (parent !== null && parent.first !== null) {
|
|
unlink_effect(effect);
|
|
}
|
|
|
|
if (DEV) {
|
|
effect.component_function = null;
|
|
}
|
|
|
|
// `first` and `child` are nulled out in destroy_effect_children
|
|
// we don't null out `parent` so that error propagation can work correctly
|
|
effect.next =
|
|
effect.prev =
|
|
effect.teardown =
|
|
effect.ctx =
|
|
effect.deps =
|
|
effect.fn =
|
|
effect.nodes_start =
|
|
effect.nodes_end =
|
|
null;
|
|
}
|
|
|
|
/**
|
|
* Detach an effect from the effect tree, freeing up memory and
|
|
* reducing the amount of work that happens on subsequent traversals
|
|
* @param {Effect} effect
|
|
*/
|
|
export function unlink_effect(effect) {
|
|
var parent = effect.parent;
|
|
var prev = effect.prev;
|
|
var next = effect.next;
|
|
|
|
if (prev !== null) prev.next = next;
|
|
if (next !== null) next.prev = prev;
|
|
|
|
if (parent !== null) {
|
|
if (parent.first === effect) parent.first = next;
|
|
if (parent.last === effect) parent.last = prev;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When a block effect is removed, we don't immediately destroy it or yank it
|
|
* out of the DOM, because it might have transitions. Instead, we 'pause' it.
|
|
* It stays around (in memory, and in the DOM) until outro transitions have
|
|
* completed, and if the state change is reversed then we _resume_ it.
|
|
* A paused effect does not update, and the DOM subtree becomes inert.
|
|
* @param {Effect} effect
|
|
* @param {() => void} [callback]
|
|
*/
|
|
export function pause_effect(effect, callback) {
|
|
/** @type {TransitionManager[]} */
|
|
var transitions = [];
|
|
|
|
pause_children(effect, transitions, true);
|
|
|
|
run_out_transitions(transitions, () => {
|
|
destroy_effect(effect);
|
|
if (callback) callback();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {TransitionManager[]} transitions
|
|
* @param {() => void} fn
|
|
*/
|
|
export function run_out_transitions(transitions, fn) {
|
|
var remaining = transitions.length;
|
|
if (remaining > 0) {
|
|
var check = () => --remaining || fn();
|
|
for (var transition of transitions) {
|
|
transition.out(check);
|
|
}
|
|
} else {
|
|
fn();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {Effect} effect
|
|
* @param {TransitionManager[]} transitions
|
|
* @param {boolean} local
|
|
*/
|
|
export function pause_children(effect, transitions, local) {
|
|
if ((effect.f & INERT) !== 0) return;
|
|
effect.f ^= INERT;
|
|
|
|
if (effect.transitions !== null) {
|
|
for (const transition of effect.transitions) {
|
|
if (transition.is_global || local) {
|
|
transitions.push(transition);
|
|
}
|
|
}
|
|
}
|
|
|
|
var child = effect.first;
|
|
|
|
while (child !== null) {
|
|
var sibling = child.next;
|
|
var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
|
|
// TODO we don't need to call pause_children recursively with a linked list in place
|
|
// it's slightly more involved though as we have to account for `transparent` changing
|
|
// through the tree.
|
|
pause_children(child, transitions, transparent ? local : false);
|
|
child = sibling;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The opposite of `pause_effect`. We call this if (for example)
|
|
* `x` becomes falsy then truthy: `{#if x}...{/if}`
|
|
* @param {Effect} effect
|
|
*/
|
|
export function resume_effect(effect) {
|
|
resume_children(effect, true);
|
|
}
|
|
|
|
/**
|
|
* @param {Effect} effect
|
|
* @param {boolean} local
|
|
*/
|
|
function resume_children(effect, local) {
|
|
if ((effect.f & INERT) === 0) return;
|
|
effect.f ^= INERT;
|
|
|
|
// Ensure the effect is marked as clean again so that any dirty child
|
|
// effects can schedule themselves for execution
|
|
if ((effect.f & CLEAN) === 0) {
|
|
effect.f ^= CLEAN;
|
|
}
|
|
|
|
// If a dependency of this effect changed while it was paused,
|
|
// schedule the effect to update
|
|
if (check_dirtiness(effect)) {
|
|
set_signal_status(effect, DIRTY);
|
|
schedule_effect(effect);
|
|
}
|
|
|
|
var child = effect.first;
|
|
|
|
while (child !== null) {
|
|
var sibling = child.next;
|
|
var transparent = (child.f & EFFECT_TRANSPARENT) !== 0 || (child.f & BRANCH_EFFECT) !== 0;
|
|
// TODO we don't need to call resume_children recursively with a linked list in place
|
|
// it's slightly more involved though as we have to account for `transparent` changing
|
|
// through the tree.
|
|
resume_children(child, transparent ? local : false);
|
|
child = sibling;
|
|
}
|
|
|
|
if (effect.transitions !== null) {
|
|
for (const transition of effect.transitions) {
|
|
if (transition.is_global || local) {
|
|
transition.in();
|
|
}
|
|
}
|
|
}
|
|
}
|