diff --git a/packages/reactivity/src/effectScope.ts b/packages/reactivity/src/effectScope.ts index 898a7cb0272..f7a2dc6f279 100644 --- a/packages/reactivity/src/effectScope.ts +++ b/packages/reactivity/src/effectScope.ts @@ -116,8 +116,12 @@ export class EffectScope { stop(fromParent?: boolean): void { if (this._active) { let i, l - for (i = 0, l = this.effects.length; i < l; i++) { - this.effects[i].stop() + // #5783 + // effects will be changed when a watch stopped. + // so we need to copy it for iteration. + const effectsToStop = this.effects.slice() + for (i = 0, l = effectsToStop.length; i < l; i++) { + effectsToStop[i].stop() } for (i = 0, l = this.cleanups.length; i < l; i++) { this.cleanups[i]() diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 85afec24ceb..40cb7d1ad59 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1378,6 +1378,43 @@ describe('api: watch', () => { expect(spy).toHaveBeenCalledTimes(2) }) + test('removing a watcher while stopping its effectScope', async () => { + const count = ref(0) + const scope = effectScope() + let watcherCalls = 0 + let cleanupCalls = 0 + + scope.run(() => { + const stop1 = watch(count, () => { + watcherCalls++ + }) + watch(count, (val, old, onCleanup) => { + watcherCalls++ + onCleanup(() => { + cleanupCalls++ + stop1() + }) + }) + watch(count, () => { + watcherCalls++ + }) + }) + + expect(watcherCalls).toBe(0) + expect(cleanupCalls).toBe(0) + + count.value++ + await nextTick() + expect(watcherCalls).toBe(3) + expect(cleanupCalls).toBe(0) + + scope.stop() + count.value++ + await nextTick() + expect(watcherCalls).toBe(3) + expect(cleanupCalls).toBe(1) + }) + it('watching sources: ref', async () => { const foo = ref([1]) const spy = vi.fn()