Skip to content

Commit b8893f9

Browse files
committed
- add state changes resulting from an $effect to a separate new batch
- schedule rerunning effects based on the sources that are dirty, not just rerunning them all blindly (excempting async effects which will have run by that time already)
1 parent 9412c58 commit b8893f9

File tree

2 files changed

+103
-80
lines changed

2 files changed

+103
-80
lines changed

packages/svelte/src/internal/client/reactivity/batch.js

Lines changed: 99 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import * as e from '../errors.js';
2828
import { flush_tasks } from '../dom/task.js';
2929
import { DEV } from 'esm-env';
3030
import { invoke_error_boundary } from '../error-handling.js';
31-
import { old_values } from './sources.js';
31+
import { mark_reactions, old_values } from './sources.js';
3232
import { unlink_effect } from './effects.js';
3333
import { unset_context } from './async.js';
3434

@@ -70,13 +70,15 @@ let last_scheduled_effect = null;
7070

7171
let is_flushing = false;
7272

73+
let flushing_sync = false;
74+
7375
export class Batch {
7476
/**
7577
* The current values of any sources that are updated in this batch
7678
* They keys of this map are identical to `this.#previous`
7779
* @type {Map<Source, any>}
7880
*/
79-
#current = new Map();
81+
current = new Map();
8082

8183
/**
8284
* The values of any sources that are updated in this batch _before_ those updates took place.
@@ -156,7 +158,7 @@ export class Batch {
156158
*
157159
* @param {Effect[]} root_effects
158160
*/
159-
#process(root_effects) {
161+
process(root_effects) {
160162
queued_root_effects = [];
161163

162164
/** @type {Map<Source, { v: unknown, wv: number }> | null} */
@@ -169,7 +171,7 @@ export class Batch {
169171
current_values = new Map();
170172
batch_deriveds = new Map();
171173

172-
for (const [source, current] of this.#current) {
174+
for (const [source, current] of this.current) {
173175
current_values.set(source, { v: source.v, wv: source.wv });
174176
source.v = current;
175177
}
@@ -300,7 +302,7 @@ export class Batch {
300302
this.#previous.set(source, value);
301303
}
302304

303-
this.#current.set(source, source.v);
305+
this.current.set(source, source.v);
304306
}
305307

306308
activate() {
@@ -346,49 +348,7 @@ export class Batch {
346348
}
347349

348350
flush_effects() {
349-
var was_updating_effect = is_updating_effect;
350-
is_flushing = true;
351-
352-
try {
353-
var flush_count = 0;
354-
set_is_updating_effect(true);
355-
356-
while (queued_root_effects.length > 0) {
357-
if (flush_count++ > 1000) {
358-
if (DEV) {
359-
var updates = new Map();
360-
361-
for (const source of this.#current.keys()) {
362-
for (const [stack, update] of source.updated ?? []) {
363-
var entry = updates.get(stack);
364-
365-
if (!entry) {
366-
entry = { error: update.error, count: 0 };
367-
updates.set(stack, entry);
368-
}
369-
370-
entry.count += update.count;
371-
}
372-
}
373-
374-
for (const update of updates.values()) {
375-
// eslint-disable-next-line no-console
376-
console.error(update.error);
377-
}
378-
}
379-
380-
infinite_loop_guard();
381-
}
382-
383-
this.#process(queued_root_effects);
384-
old_values.clear();
385-
}
386-
} finally {
387-
is_flushing = false;
388-
set_is_updating_effect(was_updating_effect);
389-
390-
last_scheduled_effect = null;
391-
}
351+
flush_effects();
392352
}
393353

394354
/**
@@ -412,19 +372,8 @@ export class Batch {
412372
this.#pending -= 1;
413373

414374
if (this.#pending === 0) {
415-
for (const e of this.#render_effects) {
416-
set_signal_status(e, DIRTY);
417-
schedule_effect(e);
418-
}
419-
420-
for (const e of this.#effects) {
421-
set_signal_status(e, DIRTY);
422-
schedule_effect(e);
423-
}
424-
425-
for (const e of this.#block_effects) {
426-
set_signal_status(e, DIRTY);
427-
schedule_effect(e);
375+
for (const source of this.current.keys()) {
376+
mark_reactions(source, DIRTY, false);
428377
}
429378

430379
this.#render_effects = [];
@@ -487,32 +436,88 @@ export function flushSync(fn) {
487436
e.flush_sync_in_effect();
488437
}
489438

490-
var result;
439+
var prev_flushing_sync = flushing_sync;
440+
flushing_sync = true;
491441

492-
const batch = Batch.ensure(false);
442+
try {
443+
var result;
493444

494-
if (fn) {
495-
batch.flush_effects();
445+
const batch = Batch.ensure(false);
496446

497-
result = fn();
498-
}
447+
if (fn) {
448+
batch.flush_effects();
499449

500-
while (true) {
501-
flush_tasks();
450+
result = fn();
451+
}
452+
453+
while (true) {
454+
flush_tasks();
455+
456+
if (queued_root_effects.length === 0) {
457+
// TODO this might need adjustment
458+
if (batch === current_batch) {
459+
batch.flush();
460+
}
461+
462+
// this would be reset in `batch.flush_effects()` but since we are early returning here,
463+
// we need to reset it here as well in case the first time there's 0 queued root effects
464+
last_scheduled_effect = null;
502465

503-
if (queued_root_effects.length === 0) {
504-
if (batch === current_batch) {
505-
batch.flush();
466+
return /** @type {T} */ (result);
506467
}
507468

508-
// this would be reset in `batch.flush_effects()` but since we are early returning here,
509-
// we need to reset it here as well in case the first time there's 0 queued root effects
510-
last_scheduled_effect = null;
469+
batch.flush_effects();
470+
}
471+
} finally {
472+
flushing_sync = prev_flushing_sync;
473+
}
474+
}
511475

512-
return /** @type {T} */ (result);
476+
function flush_effects() {
477+
var was_updating_effect = is_updating_effect;
478+
is_flushing = true;
479+
480+
try {
481+
var flush_count = 0;
482+
var batch = /** @type {Batch} */ (current_batch);
483+
set_is_updating_effect(true);
484+
485+
while (queued_root_effects.length > 0) {
486+
if (flush_count++ > 1000) {
487+
if (DEV) {
488+
var updates = new Map();
489+
490+
for (const source of batch.current.keys()) {
491+
for (const [stack, update] of source.updated ?? []) {
492+
var entry = updates.get(stack);
493+
494+
if (!entry) {
495+
entry = { error: update.error, count: 0 };
496+
updates.set(stack, entry);
497+
}
498+
499+
entry.count += update.count;
500+
}
501+
}
502+
503+
for (const update of updates.values()) {
504+
// eslint-disable-next-line no-console
505+
console.error(update.error);
506+
}
507+
}
508+
509+
infinite_loop_guard();
510+
}
511+
512+
batch = /** @type {Batch} */ (current_batch);
513+
batch.process(queued_root_effects);
514+
old_values.clear();
513515
}
516+
} finally {
517+
is_flushing = false;
518+
set_is_updating_effect(was_updating_effect);
514519

515-
batch.flush_effects();
520+
last_scheduled_effect = null;
516521
}
517522
}
518523

@@ -545,6 +550,7 @@ function flush_queued_effects(effects) {
545550
if ((effect.f & (DESTROYED | INERT)) === 0) {
546551
if (is_dirty(effect)) {
547552
var wv = write_version;
553+
var current_size = /** @type {Batch} */ (current_batch).current.size;
548554

549555
update_effect(effect);
550556

@@ -568,6 +574,22 @@ function flush_queued_effects(effects) {
568574
// if state is written in a user effect, abort and re-schedule, lest we run
569575
// effects that should be removed as a result of the state change
570576
if (write_version > wv && (effect.f & USER_EFFECT) !== 0) {
577+
// Work needs to happen in a separate batch, else prior sources would be mixed with
578+
// newly updated sources, which could lead to infinite loops when effects run over and over again.
579+
// We need to bring over the just written sources though to correctly mark the right reactions as dirty.
580+
var old_batch = /** @type {Batch} */ (current_batch);
581+
batches.delete(old_batch);
582+
current_batch = null;
583+
var new_batch = Batch.ensure(!flushing_sync);
584+
var current_idx = 0;
585+
// We're taking advantage of the spec here which says that entries in a Map are traversed by insertion order
586+
for (const source of old_batch.current) {
587+
if (current_idx >= current_size) {
588+
new_batch.capture(source[0], source[1]);
589+
}
590+
current_idx++;
591+
}
592+
i++;
571593
break;
572594
}
573595
}

packages/svelte/src/internal/client/reactivity/sources.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -301,9 +301,10 @@ export function increment(source) {
301301
/**
302302
* @param {Value} signal
303303
* @param {number} status should be DIRTY or MAYBE_DIRTY
304+
* @param {boolean} schedule_async
304305
* @returns {void}
305306
*/
306-
function mark_reactions(signal, status) {
307+
export function mark_reactions(signal, status, schedule_async = true) {
307308
var reactions = signal.reactions;
308309
if (reactions === null) return;
309310

@@ -324,13 +325,13 @@ function mark_reactions(signal, status) {
324325
}
325326

326327
// don't set a DIRTY reaction to MAYBE_DIRTY
327-
if ((flags & DIRTY) === 0) {
328+
if ((flags & DIRTY) === 0 && (schedule_async || (flags & ASYNC) === 0)) {
328329
set_signal_status(reaction, status);
329330
}
330331

331332
if ((flags & DERIVED) !== 0) {
332333
mark_reactions(/** @type {Derived} */ (reaction), MAYBE_DIRTY);
333-
} else if ((flags & DIRTY) === 0) {
334+
} else if ((flags & DIRTY) === 0 && (schedule_async || (flags & ASYNC) === 0)) {
334335
schedule_effect(/** @type {Effect} */ (reaction));
335336
}
336337
}

0 commit comments

Comments
 (0)