diff --git a/src/runtime/internal/scheduler.ts b/src/runtime/internal/scheduler.ts index 568739e4f862..c0b8e57b08e8 100644 --- a/src/runtime/internal/scheduler.ts +++ b/src/runtime/internal/scheduler.ts @@ -1,5 +1,5 @@ import { run_all } from './utils'; -import { set_current_component } from './lifecycle'; +import { current_component, set_current_component } from './lifecycle'; export const dirty_components = []; export const intros = { enabled: false }; @@ -31,23 +31,42 @@ export function add_flush_callback(fn) { flush_callbacks.push(fn); } -let flushing = false; +// flush() calls callbacks in this order: +// 1. All beforeUpdate callbacks, in order: parents before children +// 2. All bind:this callbacks, in reverse order: children before parents. +// 3. All afterUpdate callbacks, in order: parents before children. EXCEPT +// for afterUpdates called during the initial onMount, which are called in +// reverse order: children before parents. +// Since callbacks might update component values, which could trigger another +// call to flush(), the following steps guard against this: +// 1. During beforeUpdate, any updated components will be added to the +// dirty_components array and will cause a reentrant call to flush(). Because +// the flush index is kept outside the function, the reentrant call will pick +// up where the earlier call left off and go through all dirty components. The +// current_component value is saved and restored so that the reentrant call will +// not interfere with the "parent" flush() call. +// 2. bind:this callbacks cannot trigger new flush() calls. +// 3. During afterUpdate, any updated components will NOT have their afterUpdate +// callback called a second time; the seen_callbacks set, outside the flush() +// function, guarantees this behavior. const seen_callbacks = new Set(); +let flushidx = 0; // Do *not* move this inside the flush() function export function flush() { - if (flushing) return; - flushing = true; + const saved_component = current_component; do { // first, call beforeUpdate functions // and update components - for (let i = 0; i < dirty_components.length; i += 1) { - const component = dirty_components[i]; + while (flushidx < dirty_components.length) { + const component = dirty_components[flushidx]; + flushidx++; set_current_component(component); update(component.$$); } set_current_component(null); dirty_components.length = 0; + flushidx = 0; while (binding_callbacks.length) binding_callbacks.pop()(); @@ -73,8 +92,8 @@ export function flush() { } update_scheduled = false; - flushing = false; seen_callbacks.clear(); + set_current_component(saved_component); } function update($$) { diff --git a/test/runtime/samples/component-binding-onMount/Mount.svelte b/test/runtime/samples/component-binding-onMount/Mount.svelte new file mode 100644 index 000000000000..ce3b0c48f806 --- /dev/null +++ b/test/runtime/samples/component-binding-onMount/Mount.svelte @@ -0,0 +1,15 @@ + + +
++ Bound? {bound} +
diff --git a/test/runtime/samples/component-binding-onMount/_config.js b/test/runtime/samples/component-binding-onMount/_config.js new file mode 100644 index 000000000000..4ae78b588a1c --- /dev/null +++ b/test/runtime/samples/component-binding-onMount/_config.js @@ -0,0 +1,11 @@ +export default { + async test({ assert, target }) { + assert.htmlEqual(target.innerHTML, ` ++ Bound? true +
+