Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[fix] bind:this works during onMount callback in manually-created component #6920

Merged
merged 11 commits into from
Dec 13, 2021
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" />