fix: ensure UI consistency between deferred events

pull/11820/head
Dominic Gannaway 2 years ago
parent 68263c8615
commit 88a8548aca

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: ensure UI consistency between deferred events

@ -4,17 +4,18 @@ export const RENDER_EFFECT = 1 << 3;
export const BLOCK_EFFECT = 1 << 4; export const BLOCK_EFFECT = 1 << 4;
export const BRANCH_EFFECT = 1 << 5; export const BRANCH_EFFECT = 1 << 5;
export const ROOT_EFFECT = 1 << 6; export const ROOT_EFFECT = 1 << 6;
export const UNOWNED = 1 << 7; export const ACTION_EFFECT = 1 << 7;
export const CLEAN = 1 << 8; export const UNOWNED = 1 << 8;
export const DIRTY = 1 << 9; export const CLEAN = 1 << 9;
export const MAYBE_DIRTY = 1 << 10; export const DIRTY = 1 << 10;
export const INERT = 1 << 11; export const MAYBE_DIRTY = 1 << 11;
export const DESTROYED = 1 << 12; export const INERT = 1 << 12;
export const EFFECT_RAN = 1 << 13; export const DESTROYED = 1 << 13;
export const EFFECT_RAN = 1 << 14;
/** 'Transparent' effects do not create a transition boundary */ /** 'Transparent' effects do not create a transition boundary */
export const EFFECT_TRANSPARENT = 1 << 14; export const EFFECT_TRANSPARENT = 1 << 15;
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ /** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
export const LEGACY_DERIVED_PROP = 1 << 15; export const LEGACY_DERIVED_PROP = 1 << 16;
export const STATE_SYMBOL = Symbol('$state'); export const STATE_SYMBOL = Symbol('$state');
export const LOADING_ATTR_SYMBOL = Symbol(''); export const LOADING_ATTR_SYMBOL = Symbol('');

@ -1,3 +1,4 @@
import { ACTION_EFFECT } from '../../constants.js';
import { effect, render_effect } from '../../reactivity/effects.js'; import { effect, render_effect } from '../../reactivity/effects.js';
import { deep_read_state, untrack } from '../../runtime.js'; import { deep_read_state, untrack } from '../../runtime.js';
@ -15,7 +16,7 @@ export function action(dom, action, get_value) {
if (get_value && payload?.update) { if (get_value && payload?.update) {
var inited = false; var inited = false;
render_effect(() => { var signal = render_effect(() => {
var value = get_value(); var value = get_value();
// Action's update method is coarse-grained, i.e. when anything in the passed value changes, update. // Action's update method is coarse-grained, i.e. when anything in the passed value changes, update.
@ -27,6 +28,7 @@ export function action(dom, action, get_value) {
/** @type {Function} */ (payload.update)(value); /** @type {Function} */ (payload.update)(value);
} }
}); });
signal.f |= ACTION_EFFECT;
inited = true; inited = true;
} }

@ -16,7 +16,8 @@ import {
STATE_SYMBOL, STATE_SYMBOL,
BLOCK_EFFECT, BLOCK_EFFECT,
ROOT_EFFECT, ROOT_EFFECT,
LEGACY_DERIVED_PROP LEGACY_DERIVED_PROP,
ACTION_EFFECT
} from './constants.js'; } from './constants.js';
import { flush_tasks } from './dom/task.js'; import { flush_tasks } from './dom/task.js';
import { add_owner } from './dev/ownership.js'; import { add_owner } from './dev/ownership.js';
@ -600,7 +601,11 @@ async function yield_tick() {
* @returns {void} * @returns {void}
*/ */
export function schedule_effect(signal) { export function schedule_effect(signal) {
if (current_scheduler_mode === FLUSH_MICROTASK) { // If we have an action effect then we need to dispatch a standard microtask update,
// that's because actions might need to update the UI or attach/remove event listeners
// and deferring this can lead to hard-to-find bugs. If there are any deferred updates
// already, they'll be handled by the microtask instead.
if (current_scheduler_mode === FLUSH_MICROTASK || (signal.f & ACTION_EFFECT) !== 0) {
if (!is_micro_task_queued) { if (!is_micro_task_queued) {
is_micro_task_queued = true; is_micro_task_queued = true;
queueMicrotask(process_deferred); queueMicrotask(process_deferred);
@ -713,6 +718,13 @@ function process_effects(effect, collected_effects) {
*/ */
export function yield_updates(fn) { export function yield_updates(fn) {
const previous_scheduler_mode = current_scheduler_mode; const previous_scheduler_mode = current_scheduler_mode;
// If we're calling yield_updates, and we've already yielded some updates then it's likely
// that the event might might try and read from the UI. In order for this to be glitch free,
// we can flush any changes, forcing the UI to be up-to-date, so any reads from the UI work as
// expected.
if (previous_scheduler_mode !== FLUSH_YIELD && is_yield_task_queued && !is_micro_task_queued) {
flush_sync();
}
try { try {
current_scheduler_mode = FLUSH_YIELD; current_scheduler_mode = FLUSH_YIELD;
return fn(); return fn();
@ -740,6 +752,8 @@ export function flush_sync(fn, flush_previous = true) {
current_scheduler_mode = FLUSH_SYNC; current_scheduler_mode = FLUSH_SYNC;
current_queued_root_effects = root_effects; current_queued_root_effects = root_effects;
is_yield_task_queued = false;
is_micro_task_queued = false;
if (flush_previous) { if (flush_previous) {
flush_queued_root_effects(previous_queued_root_effects); flush_queued_root_effects(previous_queued_root_effects);

@ -0,0 +1,12 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target, logs }) {
const [b1] = target.querySelectorAll('button');
b1.click();
flushSync();
assert.deepEqual(logs, ['http://localhost:3000/new%20url']);
}
});

@ -0,0 +1,12 @@
<script>
let action = $state('old url');
</script>
<form action={action} onsubmit={function(event) {
console.log(this.action);
event.preventDefault();
}}>
<button type="submit" onclick={() => {
action = 'new url';
}}>Submit</button>
</form>

@ -0,0 +1,13 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
test({ assert, target, logs }) {
const [b1] = target.querySelectorAll('button');
b1.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
b1.click();
flushSync();
assert.deepEqual(logs, []);
}
});

@ -0,0 +1,9 @@
<script>
let disabled = $state(false);
</script>
<button disabled={disabled} onmouseup={() => {
disabled = true;
}} onclick={() => {
console.log('I should not be invoked');
}}>Click me!</button>
Loading…
Cancel
Save