reimplement beforeUpdate/afterUpdate

pull/10408/head
Rich Harris 3 years ago
parent 36ed4675bb
commit baaca911cc

@ -346,12 +346,6 @@ function execute_signal_fn(signal) {
current_skip_consumer = !is_flushing_effect && (flags & UNOWNED) !== 0;
current_untracking = false;
// Render effects are invoked when the UI is about to be updated - run beforeUpdate at that point
if (is_render_effect && current_component_context?.u != null) {
// update_callbacks.execute()
current_component_context.u.e();
}
try {
let res;
if (is_render_effect) {
@ -1236,19 +1230,6 @@ export function set_signal_value(signal, value) {
}
}
mark_signal_consumers(signal, DIRTY, true);
// If we have afterUpdates locally on the component, but we're within a render effect
// then we will need to manually invoke the beforeUpdate/afterUpdate logic.
// TODO: should we put this being a is_runes check and only run it in non-runes mode?
if (current_effect === null && current_queued_pre_and_render_effects.length === 0) {
const update_callbacks = component_context?.u;
if (update_callbacks != null) {
run_all(update_callbacks.b);
const managed = managed_effect(() => {
destroy_signal(managed);
run_all(update_callbacks.a);
});
}
}
// @ts-expect-error
if (DEV && signal.inspect) {
@ -1327,7 +1308,12 @@ export function derived_safe_equal(init) {
/*#__NO_SIDE_EFFECTS__*/
export function source(initial_value) {
const source = create_source_signal(SOURCE | CLEAN, initial_value);
source.x = current_component_context;
if (current_component_context) {
source.x = current_component_context;
current_component_context.d.push(source);
}
return source;
}
@ -1914,6 +1900,8 @@ export function push(props, runes = false) {
m: false,
// parent
p: current_component_context,
// signals
d: [],
// props
s: props,
// runes

@ -36,6 +36,8 @@ export type Store<V> = {
// when the JS VM JITs the code.
export type ComponentContext = {
/** local signals */
d: Signal<any>[];
/** props */
s: Record<string, unknown>;
/** accessors */
@ -53,11 +55,9 @@ export type ComponentContext = {
/** update_callbacks */
u: null | {
/** before */
b: Array<() => void>;
b: null | Array<() => void>;
/** after */
a: Array<() => void>;
/** execute */
e: () => void;
a: null | Array<() => void>;
};
};

@ -19,3 +19,8 @@ export function run_all(arr) {
arr[i]();
}
}
/** @param {Function} fn */
export function run(fn) {
fn();
}

@ -6,9 +6,12 @@ import {
managed_effect,
untrack,
user_effect,
flush_local_render_effects
flush_local_render_effects,
pre_effect,
get
} from '../internal/client/runtime.js';
import { is_array } from '../internal/client/utils.js';
import { get_descriptors, is_array } from '../internal/client/utils.js';
import { run } from '../internal/common.js';
/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
@ -155,43 +158,33 @@ export function createEventDispatcher() {
};
}
function init_update_callbacks() {
let called_before = false;
let called_after = false;
/** @param {import('../internal/client/types.js').ComponentContext} context */
function init_update_callbacks(context) {
/** @type {NonNullable<import('../internal/client/types.js').ComponentContext['u']>} */
const update_callbacks = {
b: [],
a: [],
e() {
if (!called_before) {
called_before = true;
// TODO somehow beforeUpdate ran twice on mount in Svelte 4 if it causes a render
// possibly strategy to get this back if needed: analyse beforeUpdate function for assignements to state,
// if yes, add a call to the component to force-run beforeUpdate once.
untrack(() => update_callbacks.b.forEach(/** @param {any} c */ (c) => c()));
flush_local_render_effects();
// beforeUpdate can run again once if afterUpdate causes another update,
// but afterUpdate shouldn't be called again in that case to prevent infinite loops
if (!called_after) {
user_effect(() => {
called_before = false;
called_after = true;
untrack(() => update_callbacks.a.forEach(/** @param {any} c */ (c) => c()));
// managed_effect so that it's not cleaned up when the parent effect is cleaned up
const managed = managed_effect(() => {
destroy_signal(managed);
called_after = false;
});
});
} else {
user_effect(() => {
called_before = false;
});
}
}
}
a: []
};
function observe_all() {
for (const signal of context.d) get(signal);
const props = get_descriptors(context.s);
for (const descriptor of Object.values(props)) {
if (descriptor.get) descriptor.get();
}
}
pre_effect(() => {
observe_all();
update_callbacks.b.forEach(run);
});
user_effect(() => {
observe_all();
update_callbacks.a.forEach(run);
});
return update_callbacks;
}
@ -210,12 +203,31 @@ function init_update_callbacks() {
* @returns {void}
*/
export function beforeUpdate(fn) {
const component_context = current_component_context;
if (component_context === null) {
const context = current_component_context;
if (context === null) {
throw new Error('beforeUpdate can only be used during component initialisation.');
}
(component_context.u ??= init_update_callbacks()).b.push(fn);
context.u ??= { a: null, b: null };
if (context.u.a) {
context.u.a.push(fn);
fn();
} else {
const callbacks = (context.u.a = [fn]);
pre_effect(() => {
for (const signal of context.d) get(signal);
const props = get_descriptors(context.s);
for (const descriptor of Object.values(props)) {
if (descriptor.get) descriptor.get();
}
callbacks.forEach(run);
});
}
}
/**
@ -231,12 +243,30 @@ export function beforeUpdate(fn) {
* @returns {void}
*/
export function afterUpdate(fn) {
const component_context = current_component_context;
if (component_context === null) {
const context = current_component_context;
if (context === null) {
throw new Error('afterUpdate can only be used during component initialisation.');
}
(component_context.u ??= init_update_callbacks()).a.push(fn);
context.u ??= { a: null, b: null };
if (context.u.b) {
context.u.b.push(fn);
} else {
const callbacks = (context.u.b = [fn]);
user_effect(() => {
for (const signal of context.d) get(signal);
const props = get_descriptors(context.s);
for (const descriptor of Object.values(props)) {
if (descriptor.get) descriptor.get();
}
callbacks.forEach(run);
});
}
}
// TODO bring implementations in here

@ -1,4 +1,4 @@
import { noop } from '../internal/common.js';
import { noop, run } from '../internal/common.js';
import { subscribe_to_store } from './utils.js';
/**
@ -106,11 +106,6 @@ export function writable(value, start = noop) {
return { set, update, subscribe };
}
/** @param {Function} fn */
function run(fn) {
return fn();
}
/**
* @param {Function[]} fns
* @returns {void}

@ -37,7 +37,7 @@ export default test({
target.innerHTML,
`
<button>a: 1</button>
<button>b: 2</button>
<button>b: 1</button>
<p>a: 1</p>
`
);

Loading…
Cancel
Save