fix(reactivity): avoid infinite recursion from side effects in computed getter (#10232)

close #10214
This commit is contained in:
Johnson Chu 2024-02-06 18:44:09 +08:00 committed by GitHub
parent 6c7e0bd88f
commit 0bced13ee5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 58 additions and 16 deletions

View File

@ -482,8 +482,12 @@ 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(c2.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
expect(c3.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
})
it('should work when chained(ref+computed)', () => {
@ -550,8 +554,12 @@ 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(c2.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
expect(c3.effect._dirtyLevel).toBe(
DirtyLevels.MaybeDirty_ComputedSideEffect,
)
v1.value.v.value = 999
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
@ -581,4 +589,26 @@ describe('reactivity/computed', () => {
await nextTick()
expect(serializeInner(root)).toBe(`2`)
})
it('should not trigger effect scheduler by recurse computed effect', async () => {
const v = ref('Hello')
const c = computed(() => {
v.value += ' World'
return v.value
})
const Comp = {
setup: () => {
return () => c.value
},
}
const root = nodeOps.createElement('div')
render(h(Comp), root)
await nextTick()
expect(serializeInner(root)).toBe('Hello World')
v.value += ' World'
await nextTick()
expect(serializeInner(root)).toBe('Hello World World World World')
})
})

View File

@ -43,7 +43,13 @@ export class ComputedRefImpl<T> {
) {
this.effect = new ReactiveEffect(
() => getter(this._value),
() => triggerRefValue(this, DirtyLevels.MaybeDirty),
() =>
triggerRefValue(
this,
this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
? DirtyLevels.MaybeDirty_ComputedSideEffect
: DirtyLevels.MaybeDirty,
),
)
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
@ -60,8 +66,8 @@ export class ComputedRefImpl<T> {
triggerRefValue(self, DirtyLevels.Dirty)
}
trackRefValue(self)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty) {
triggerRefValue(self, DirtyLevels.MaybeDirty)
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
}
return self._value
}

View File

@ -25,6 +25,7 @@ export enum ReactiveFlags {
export enum DirtyLevels {
NotDirty = 0,
QueryingDirty = 1,
MaybeDirty = 2,
Dirty = 3,
MaybeDirty_ComputedSideEffect = 2,
MaybeDirty = 3,
Dirty = 4,
}

View File

@ -76,7 +76,10 @@ export class ReactiveEffect<T = any> {
}
public get dirty() {
if (this._dirtyLevel === DirtyLevels.MaybeDirty) {
if (
this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
this._dirtyLevel === DirtyLevels.MaybeDirty
) {
this._dirtyLevel = DirtyLevels.QueryingDirty
pauseTracking()
for (let i = 0; i < this._depsLength; i++) {
@ -309,7 +312,10 @@ export function triggerEffects(
effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
}
effect.trigger()
if (!effect._runnings || effect.allowRecurse) {
if (
(!effect._runnings || effect.allowRecurse) &&
effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
) {
effect._shouldSchedule = false
if (effect.scheduler) {
queueEffectSchedulers.push(effect.scheduler)

View File

@ -49,11 +49,10 @@ export function trackRefValue(ref: RefBase<any>) {
ref = toRaw(ref)
trackEffect(
activeEffect,
ref.dep ||
(ref.dep = createDep(
() => (ref.dep = undefined),
ref instanceof ComputedRefImpl ? ref : undefined,
)),
(ref.dep ??= createDep(
() => (ref.dep = undefined),
ref instanceof ComputedRefImpl ? ref : undefined,
)),
__DEV__
? {
target: ref,