From d3f5e6e5319b4ffaa55ca9a2ea3d95d78e76fa58 Mon Sep 17 00:00:00 2001 From: Tycho Date: Thu, 3 Oct 2024 23:16:52 +0800 Subject: [PATCH] fix(reactivity): prevent overwriting `next` property during batch processing (#12075) close #12072 --- packages/reactivity/__tests__/watch.spec.ts | 17 +++++++++++ packages/reactivity/src/computed.ts | 2 +- packages/reactivity/src/effect.ts | 33 ++++++++++++--------- 3 files changed, 37 insertions(+), 15 deletions(-) diff --git a/packages/reactivity/__tests__/watch.spec.ts b/packages/reactivity/__tests__/watch.spec.ts index 882e8a245fa..245acfd63be 100644 --- a/packages/reactivity/__tests__/watch.spec.ts +++ b/packages/reactivity/__tests__/watch.spec.ts @@ -260,4 +260,21 @@ describe('watch', () => { src.value = 10 expect(spy).toHaveBeenCalledTimes(2) }) + + test('should ensure correct execution order in batch processing', () => { + const dummy: number[] = [] + const n1 = ref(0) + const n2 = ref(0) + const sum = computed(() => n1.value + n2.value) + watch(n1, () => { + dummy.push(1) + n2.value++ + }) + watch(sum, () => dummy.push(2)) + watch(n1, () => dummy.push(3)) + + n1.value++ + + expect(dummy).toEqual([1, 2, 3]) + }) }) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 628cf46367b..ea798e201d4 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -121,7 +121,7 @@ export class ComputedRefImpl implements Subscriber { // avoid infinite self recursion activeSub !== this ) { - batch(this) + batch(this, true) return true } else if (__DEV__) { // TODO warn diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index dad800d146d..243518839b3 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -234,9 +234,15 @@ export class ReactiveEffect let batchDepth = 0 let batchedSub: Subscriber | undefined +let batchedComputed: Subscriber | undefined -export function batch(sub: Subscriber): void { +export function batch(sub: Subscriber, isComputed = false): void { sub.flags |= EffectFlags.NOTIFIED + if (isComputed) { + sub.next = batchedComputed + batchedComputed = sub + return + } sub.next = batchedSub batchedSub = sub } @@ -257,24 +263,23 @@ export function endBatch(): void { return } + if (batchedComputed) { + let e: Subscriber | undefined = batchedComputed + batchedComputed = undefined + while (e) { + const next: Subscriber | undefined = e.next + e.next = undefined + e.flags &= ~EffectFlags.NOTIFIED + e = next + } + } + let error: unknown while (batchedSub) { let e: Subscriber | undefined = batchedSub - let next: Subscriber | undefined - // 1st pass: clear notified flags for computed upfront - // we use the ACTIVE flag as a discriminator between computed and effect, - // since NOTIFIED is useless for an inactive effect anyway. - while (e) { - if (!(e.flags & EffectFlags.ACTIVE)) { - e.flags &= ~EffectFlags.NOTIFIED - } - e = e.next - } - e = batchedSub batchedSub = undefined - // 2nd pass: run effects while (e) { - next = e.next + const next: Subscriber | undefined = e.next e.next = undefined e.flags &= ~EffectFlags.NOTIFIED if (e.flags & EffectFlags.ACTIVE) {