fix(reactivity): avoid infinite loop when render access a side effect computed (#11135)

close #11121
This commit is contained in:
Doctor Wu 2024-06-14 23:51:19 +08:00 committed by GitHub
parent a23e99bedf
commit 8296e19855
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 129 additions and 12 deletions

View File

@ -456,6 +456,78 @@ describe('reactivity/computed', () => {
expect(fnSpy).toBeCalledTimes(2) expect(fnSpy).toBeCalledTimes(2)
}) })
it('should mark dirty as MaybeDirty_ComputedSideEffect_Origin', () => {
const v = ref(1)
const c = computed(() => {
v.value += 1
return v.value
})
c.value
expect(c.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
)
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
it('should not infinite re-run effect when effect access original side effect computed', async () => {
const spy = vi.fn()
const v = ref(0)
const c = computed(() => {
v.value += 1
return v.value
})
const Comp = {
setup: () => {
return () => {
spy()
return v.value + c.value
}
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(spy).toBeCalledTimes(1)
await nextTick()
expect(c.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
)
expect(serializeInner(root)).toBe('2')
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
it('should not infinite re-run effect when effect access chained side effect computed', async () => {
const spy = vi.fn()
const v = ref(0)
const c1 = computed(() => {
v.value += 1
return v.value
})
const c2 = computed(() => v.value + c1.value)
const Comp = {
setup: () => {
return () => {
spy()
return v.value + c1.value + c2.value
}
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
expect(spy).toBeCalledTimes(1)
await nextTick()
expect(c1.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
)
expect(c2.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
expect(serializeInner(root)).toBe('4')
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
})
it('should chained recurse effects clear dirty after trigger', () => { it('should chained recurse effects clear dirty after trigger', () => {
const v = ref(1) const v = ref(1)
const c1 = computed(() => v.value) const c1 = computed(() => v.value)
@ -482,7 +554,9 @@ describe('reactivity/computed', () => {
c3.value c3.value
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) expect(c1.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
)
expect(c2.effect._dirtyLevel).toBe( expect(c2.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect, DirtyLevels.MaybeDirty_ComputedSideEffect,
) )
@ -502,7 +576,9 @@ describe('reactivity/computed', () => {
}) })
const c2 = computed(() => v.value + c1.value) const c2 = computed(() => v.value + c1.value)
expect(c2.value).toBe('0foo') expect(c2.value).toBe('0foo')
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty) expect(c2.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
expect(c2.value).toBe('1foo') expect(c2.value).toBe('1foo')
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
}) })
@ -523,8 +599,12 @@ describe('reactivity/computed', () => {
c2.value c2.value
}) })
expect(fnSpy).toBeCalledTimes(1) expect(fnSpy).toBeCalledTimes(1)
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) expect(c1.effect._dirtyLevel).toBe(
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty) DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
)
expect(c2.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
v.value = 2 v.value = 2
expect(fnSpy).toBeCalledTimes(2) expect(fnSpy).toBeCalledTimes(2)
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
@ -557,7 +637,9 @@ describe('reactivity/computed', () => {
expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty) expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
c3.value c3.value
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty) expect(c1.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
)
expect(c2.effect._dirtyLevel).toBe( expect(c2.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect, DirtyLevels.MaybeDirty_ComputedSideEffect,
) )
@ -611,11 +693,18 @@ describe('reactivity/computed', () => {
render(h(Comp), root) render(h(Comp), root)
await nextTick() await nextTick()
expect(c.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
)
expect(serializeInner(root)).toBe('Hello World') expect(serializeInner(root)).toBe('Hello World')
v.value += ' World' v.value += ' World'
expect(c.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
await nextTick() await nextTick()
expect(serializeInner(root)).toBe('Hello World World World World') expect(c.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
)
expect(serializeInner(root)).toBe('Hello World World World')
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned() expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
}) })

View File

@ -78,7 +78,10 @@ export class ComputedRefImpl<T> {
triggerRefValue(self, DirtyLevels.Dirty) triggerRefValue(self, DirtyLevels.Dirty)
} }
trackRefValue(self) trackRefValue(self)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) { if (
self.effect._dirtyLevel >=
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin
) {
if (__DEV__ && (__TEST__ || this._warnRecursive)) { if (__DEV__ && (__TEST__ || this._warnRecursive)) {
warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter) warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter)
} }

View File

@ -23,9 +23,10 @@ export enum ReactiveFlags {
} }
export enum DirtyLevels { export enum DirtyLevels {
NotDirty = 0, NotDirty,
QueryingDirty = 1, QueryingDirty,
MaybeDirty_ComputedSideEffect = 2, MaybeDirty_ComputedSideEffect_Origin,
MaybeDirty = 3, MaybeDirty_ComputedSideEffect,
Dirty = 4, MaybeDirty,
Dirty,
} }

View File

@ -76,6 +76,9 @@ export class ReactiveEffect<T = any> {
} }
public get dirty() { public get dirty() {
// treat original side effect computed as not dirty to avoid infinite loop
if (this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect_Origin)
return false
if ( if (
this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect || this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
this._dirtyLevel === DirtyLevels.MaybeDirty this._dirtyLevel === DirtyLevels.MaybeDirty
@ -85,6 +88,13 @@ export class ReactiveEffect<T = any> {
for (let i = 0; i < this._depsLength; i++) { for (let i = 0; i < this._depsLength; i++) {
const dep = this.deps[i] const dep = this.deps[i]
if (dep.computed) { if (dep.computed) {
// treat chained side effect computed as dirty to force it re-run
// since we know the original side effect computed is dirty
if (
dep.computed.effect._dirtyLevel ===
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin
)
return true
triggerComputed(dep.computed) triggerComputed(dep.computed)
if (this._dirtyLevel >= DirtyLevels.Dirty) { if (this._dirtyLevel >= DirtyLevels.Dirty) {
break break
@ -296,6 +306,12 @@ export function triggerEffects(
) { ) {
pauseScheduling() pauseScheduling()
for (const effect of dep.keys()) { for (const effect of dep.keys()) {
if (!dep.computed && effect.computed) {
if (dep.get(effect) === effect._trackId && effect._runnings > 0) {
effect._dirtyLevel = DirtyLevels.MaybeDirty_ComputedSideEffect_Origin
continue
}
}
// dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
let tracking: boolean | undefined let tracking: boolean | undefined
if ( if (
@ -303,6 +319,14 @@ export function triggerEffects(
(tracking ??= dep.get(effect) === effect._trackId) (tracking ??= dep.get(effect) === effect._trackId)
) { ) {
effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
// always schedule if the computed is original side effect
// since we know it is actually dirty
if (
effect.computed &&
effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect_Origin
) {
effect._shouldSchedule = true
}
effect._dirtyLevel = dirtyLevel effect._dirtyLevel = dirtyLevel
} }
if ( if (