Skip to content
51 changes: 43 additions & 8 deletions packages/reactivity/__tests__/computed.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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)', () => {
Expand All @@ -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', () => {
Expand All @@ -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<any>({})
const consumer = computed(() => {
Expand Down
15 changes: 9 additions & 6 deletions packages/reactivity/__tests__/effect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
6 changes: 1 addition & 5 deletions packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -44,7 +44,6 @@ export class ComputedRefImpl<T> {
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
Expand All @@ -60,9 +59,6 @@ export class ComputedRefImpl<T> {
}
}
trackRefValue(self)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty) {
triggerRefValue(self, DirtyLevels.MaybeDirty)
}
return self._value
}

Expand Down
50 changes: 32 additions & 18 deletions packages/reactivity/src/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,24 +100,38 @@ export class ReactiveEffect<T = any> {
}

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() {
Expand Down
2 changes: 0 additions & 2 deletions packages/runtime-core/__tests__/rendererComponent.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,6 @@ describe('renderer: component', () => {

const root = nodeOps.createElement('div')
render(h(App), root)
expect(serializeInner(root)).toBe(`<div>0</div><div>1</div>`)
await nextTick()
expect(serializeInner(root)).toBe(`<div>1</div><div>1</div>`)
})

Expand Down