Skip to content
33 changes: 26 additions & 7 deletions src/runtime/internal/scheduler.ts
Original file line number Diff line number Diff line change
@@ -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 };
Expand Down Expand Up @@ -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()();

Expand All @@ -73,8 +92,8 @@ export function flush() {
}

update_scheduled = false;
flushing = false;
seen_callbacks.clear();
set_current_component(saved_component);
}

function update($$) {
Expand Down
15 changes: 15 additions & 0 deletions test/runtime/samples/component-binding-onMount/Mount.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
import { onMount } from 'svelte';

let element;
let bound = false;
onMount(() => {
if (element) bound = true;
});

</script>

<div bind:this={element}></div>
<p>
Bound? {bound}
</p>
11 changes: 11 additions & 0 deletions test/runtime/samples/component-binding-onMount/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
async test({ assert, target }) {
assert.htmlEqual(target.innerHTML, `
<div id="target"><div></div>
<p>
Bound? true
</p>
</div>
`);
}
};
13 changes: 13 additions & 0 deletions test/runtime/samples/component-binding-onMount/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script>
import Mount from './Mount.svelte';
import { onMount } from 'svelte';

onMount(() => {
const component = new Mount({
target: document.querySelector('#target'),
props: {},
});
});
</script>

<div id="target" />