diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index 860e4dab154..e28126155c3 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -1,4 +1,11 @@ -import { h, nextTick, nodeOps, render, serializeInner } from '@vue/runtime-test' +import { + h, + nextTick, + nodeOps, + render, + serializeInner, + shallowRef, +} from '@vue/runtime-test' import { type DebuggerEvent, ITERATE_KEY, @@ -480,9 +487,9 @@ describe('reactivity/computed', () => { c3.value - expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) - expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) + expect(c1.effect._dirtyLevel).toBe(DirtyLevels.NotDirty) + expect(c2.effect._dirtyLevel).toBe(DirtyLevels.NotDirty) + expect(c3.effect._dirtyLevel).toBe(DirtyLevels.NotDirty) }) it('should work when chained(ref+computed)', () => { @@ -494,9 +501,8 @@ describe('reactivity/computed', () => { return 'foo' }) const c2 = computed(() => v.value + c1.value) - expect(c2.value).toBe('0foo') - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty) expect(c2.value).toBe('1foo') + expect(c2.effect._dirtyLevel).toBe(DirtyLevels.NotDirty) }) it('should trigger effect even computed already dirty', () => { @@ -515,12 +521,41 @@ describe('reactivity/computed', () => { c2.value }) expect(fnSpy).toBeCalledTimes(1) - expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) - expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty) + expect(c1.effect._dirtyLevel).toBe(DirtyLevels.NotDirty) + expect(c2.effect._dirtyLevel).toBe(DirtyLevels.NotDirty) v.value = 2 expect(fnSpy).toBeCalledTimes(2) }) + it('should not override queried MaybeDirty result', () => { + class Item { + v = ref(0) + } + const v1 = shallowRef() + const v2 = ref(false) + const c1 = computed(() => { + let c = v1.value + if (!v1.value) { + c = new Item() + v1.value = c + } + return c.v.value + }) + const c2 = computed(() => { + if (!v2.value) return 'no' + return c1.value ? 'yes' : 'no' + }) + const c3 = computed(() => c2.value) + + c3.value + v2.value = true + c3.value + v1.value.v.value = 999 + + expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) + expect(c3.value).toBe('yes') + }) + it('should be not dirty after deps mutate (mutate deps in computed)', async () => { const state = reactive({}) const consumer = computed(() => { diff --git a/packages/reactivity/__tests__/effect.spec.ts b/packages/reactivity/__tests__/effect.spec.ts index bd26934f1ce..6b5cf43771d 100644 --- a/packages/reactivity/__tests__/effect.spec.ts +++ b/packages/reactivity/__tests__/effect.spec.ts @@ -376,11 +376,13 @@ describe('reactivity/effect', () => { const counterSpy = vi.fn(() => counter.num++) effect(counterSpy) - expect(counter.num).toBe(1) - expect(counterSpy).toHaveBeenCalledTimes(1) + expect(`Effect is recursively triggering itself`).toHaveBeenWarned() + expect(counter.num).toBe(100) + expect(counterSpy).toHaveBeenCalledTimes(100) counter.num = 4 - expect(counter.num).toBe(5) - expect(counterSpy).toHaveBeenCalledTimes(2) + expect(`Effect is recursively triggering itself`).toHaveBeenWarned() + expect(counter.num).toBe(104) + expect(counterSpy).toHaveBeenCalledTimes(200) }) it('should avoid infinite recursive loops when use Array.prototype.push/unshift/pop/shift', () => { @@ -415,8 +417,9 @@ describe('reactivity/effect', () => { } }) effect(numSpy) - expect(counter.num).toEqual(10) - expect(numSpy).toHaveBeenCalledTimes(10) + expect(counter.num).toEqual(109) + expect(numSpy).toHaveBeenCalledTimes(109) + expect(`Effect is recursively triggering itself`).toHaveBeenWarned() }) it('should avoid infinite loops with other effects', () => { diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 03459c7dfb6..9f4d105c0ea 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -1,4 +1,4 @@ -import { type DebuggerOptions, ReactiveEffect, scheduleEffects } from './effect' +import { type DebuggerOptions, ReactiveEffect } from './effect' import { type Ref, trackRefValue, triggerRefValue } from './ref' import { NOOP, hasChanged, isFunction } from '@vue/shared' import { toRaw } from './reactive' @@ -44,7 +44,6 @@ export class ComputedRefImpl { this.effect = new ReactiveEffect( () => getter(this._value), () => triggerRefValue(this, DirtyLevels.MaybeDirty), - () => this.dep && scheduleEffects(this.dep), ) this.effect.computed = this this.effect.active = this._cacheable = !isSSR @@ -60,9 +59,6 @@ export class ComputedRefImpl { } } trackRefValue(self) - if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty) { - triggerRefValue(self, DirtyLevels.MaybeDirty) - } return self._value } diff --git a/packages/reactivity/src/effect.ts b/packages/reactivity/src/effect.ts index a41cd4986f6..339b237b48f 100644 --- a/packages/reactivity/src/effect.ts +++ b/packages/reactivity/src/effect.ts @@ -100,24 +100,38 @@ export class ReactiveEffect { } run() { - this._dirtyLevel = DirtyLevels.NotDirty - if (!this.active) { - return this.fn() - } - let lastShouldTrack = shouldTrack - let lastEffect = activeEffect - try { - shouldTrack = true - activeEffect = this - this._runnings++ - preCleanupEffect(this) - return this.fn() - } finally { - postCleanupEffect(this) - this._runnings-- - activeEffect = lastEffect - shouldTrack = lastShouldTrack - } + let result + + let maxRecursion = 100 + do { + this._dirtyLevel = DirtyLevels.NotDirty + if (!this.active) { + return this.fn() + } + let lastShouldTrack = shouldTrack + let lastEffect = activeEffect + try { + shouldTrack = true + activeEffect = this + this._runnings++ + preCleanupEffect(this) + result = this.fn() + } finally { + postCleanupEffect(this) + this._runnings-- + activeEffect = lastEffect + shouldTrack = lastShouldTrack + } + if (--maxRecursion == 0) { + if (__DEV__) { + console.warn('Effect is recursively triggering itself') + // We regard the computed as being done to avoid reactivity issues as in #10185. + this._dirtyLevel = DirtyLevels.NotDirty + } + return result + } + } while (this._dirtyLevel >= DirtyLevels.MaybeDirty) + return result } stop() { diff --git a/packages/runtime-core/__tests__/rendererComponent.spec.ts b/packages/runtime-core/__tests__/rendererComponent.spec.ts index c5faa05de83..238ab1ba7dc 100644 --- a/packages/runtime-core/__tests__/rendererComponent.spec.ts +++ b/packages/runtime-core/__tests__/rendererComponent.spec.ts @@ -135,8 +135,6 @@ describe('renderer: component', () => { const root = nodeOps.createElement('div') render(h(App), root) - expect(serializeInner(root)).toBe(`
0
1
`) - await nextTick() expect(serializeInner(root)).toBe(`
1
1
`) })