Skip to content

Commit 8296e19

Browse files
authoredJun 14, 2024··
fix(reactivity): avoid infinite loop when render access a side effect computed (#11135)
close #11121
1 parent a23e99b commit 8296e19

File tree

4 files changed

+129
-12
lines changed

4 files changed

+129
-12
lines changed
 

‎packages/reactivity/__tests__/computed.spec.ts

+95-6
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,78 @@ describe('reactivity/computed', () => {
456456
expect(fnSpy).toBeCalledTimes(2)
457457
})
458458

459+
it('should mark dirty as MaybeDirty_ComputedSideEffect_Origin', () => {
460+
const v = ref(1)
461+
const c = computed(() => {
462+
v.value += 1
463+
return v.value
464+
})
465+
466+
c.value
467+
expect(c.effect._dirtyLevel).toBe(
468+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
469+
)
470+
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
471+
})
472+
473+
it('should not infinite re-run effect when effect access original side effect computed', async () => {
474+
const spy = vi.fn()
475+
const v = ref(0)
476+
const c = computed(() => {
477+
v.value += 1
478+
return v.value
479+
})
480+
const Comp = {
481+
setup: () => {
482+
return () => {
483+
spy()
484+
return v.value + c.value
485+
}
486+
},
487+
}
488+
const root = nodeOps.createElement('div')
489+
490+
render(h(Comp), root)
491+
expect(spy).toBeCalledTimes(1)
492+
await nextTick()
493+
expect(c.effect._dirtyLevel).toBe(
494+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
495+
)
496+
expect(serializeInner(root)).toBe('2')
497+
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
498+
})
499+
500+
it('should not infinite re-run effect when effect access chained side effect computed', async () => {
501+
const spy = vi.fn()
502+
const v = ref(0)
503+
const c1 = computed(() => {
504+
v.value += 1
505+
return v.value
506+
})
507+
const c2 = computed(() => v.value + c1.value)
508+
const Comp = {
509+
setup: () => {
510+
return () => {
511+
spy()
512+
return v.value + c1.value + c2.value
513+
}
514+
},
515+
}
516+
const root = nodeOps.createElement('div')
517+
518+
render(h(Comp), root)
519+
expect(spy).toBeCalledTimes(1)
520+
await nextTick()
521+
expect(c1.effect._dirtyLevel).toBe(
522+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
523+
)
524+
expect(c2.effect._dirtyLevel).toBe(
525+
DirtyLevels.MaybeDirty_ComputedSideEffect,
526+
)
527+
expect(serializeInner(root)).toBe('4')
528+
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
529+
})
530+
459531
it('should chained recurse effects clear dirty after trigger', () => {
460532
const v = ref(1)
461533
const c1 = computed(() => v.value)
@@ -482,7 +554,9 @@ describe('reactivity/computed', () => {
482554

483555
c3.value
484556

485-
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
557+
expect(c1.effect._dirtyLevel).toBe(
558+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
559+
)
486560
expect(c2.effect._dirtyLevel).toBe(
487561
DirtyLevels.MaybeDirty_ComputedSideEffect,
488562
)
@@ -502,7 +576,9 @@ describe('reactivity/computed', () => {
502576
})
503577
const c2 = computed(() => v.value + c1.value)
504578
expect(c2.value).toBe('0foo')
505-
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
579+
expect(c2.effect._dirtyLevel).toBe(
580+
DirtyLevels.MaybeDirty_ComputedSideEffect,
581+
)
506582
expect(c2.value).toBe('1foo')
507583
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
508584
})
@@ -523,8 +599,12 @@ describe('reactivity/computed', () => {
523599
c2.value
524600
})
525601
expect(fnSpy).toBeCalledTimes(1)
526-
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
527-
expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
602+
expect(c1.effect._dirtyLevel).toBe(
603+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
604+
)
605+
expect(c2.effect._dirtyLevel).toBe(
606+
DirtyLevels.MaybeDirty_ComputedSideEffect,
607+
)
528608
v.value = 2
529609
expect(fnSpy).toBeCalledTimes(2)
530610
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
@@ -557,7 +637,9 @@ describe('reactivity/computed', () => {
557637
expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
558638

559639
c3.value
560-
expect(c1.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
640+
expect(c1.effect._dirtyLevel).toBe(
641+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
642+
)
561643
expect(c2.effect._dirtyLevel).toBe(
562644
DirtyLevels.MaybeDirty_ComputedSideEffect,
563645
)
@@ -611,11 +693,18 @@ describe('reactivity/computed', () => {
611693

612694
render(h(Comp), root)
613695
await nextTick()
696+
expect(c.effect._dirtyLevel).toBe(
697+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
698+
)
614699
expect(serializeInner(root)).toBe('Hello World')
615700

616701
v.value += ' World'
702+
expect(c.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
617703
await nextTick()
618-
expect(serializeInner(root)).toBe('Hello World World World World')
704+
expect(c.effect._dirtyLevel).toBe(
705+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin,
706+
)
707+
expect(serializeInner(root)).toBe('Hello World World World')
619708
expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
620709
})
621710

‎packages/reactivity/src/computed.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,10 @@ export class ComputedRefImpl<T> {
7878
triggerRefValue(self, DirtyLevels.Dirty)
7979
}
8080
trackRefValue(self)
81-
if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
81+
if (
82+
self.effect._dirtyLevel >=
83+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin
84+
) {
8285
if (__DEV__ && (__TEST__ || this._warnRecursive)) {
8386
warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter)
8487
}

‎packages/reactivity/src/constants.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ export enum ReactiveFlags {
2323
}
2424

2525
export enum DirtyLevels {
26-
NotDirty = 0,
27-
QueryingDirty = 1,
28-
MaybeDirty_ComputedSideEffect = 2,
29-
MaybeDirty = 3,
30-
Dirty = 4,
26+
NotDirty,
27+
QueryingDirty,
28+
MaybeDirty_ComputedSideEffect_Origin,
29+
MaybeDirty_ComputedSideEffect,
30+
MaybeDirty,
31+
Dirty,
3132
}

‎packages/reactivity/src/effect.ts

+24
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ export class ReactiveEffect<T = any> {
7676
}
7777

7878
public get dirty() {
79+
// treat original side effect computed as not dirty to avoid infinite loop
80+
if (this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect_Origin)
81+
return false
7982
if (
8083
this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
8184
this._dirtyLevel === DirtyLevels.MaybeDirty
@@ -85,6 +88,13 @@ export class ReactiveEffect<T = any> {
8588
for (let i = 0; i < this._depsLength; i++) {
8689
const dep = this.deps[i]
8790
if (dep.computed) {
91+
// treat chained side effect computed as dirty to force it re-run
92+
// since we know the original side effect computed is dirty
93+
if (
94+
dep.computed.effect._dirtyLevel ===
95+
DirtyLevels.MaybeDirty_ComputedSideEffect_Origin
96+
)
97+
return true
8898
triggerComputed(dep.computed)
8999
if (this._dirtyLevel >= DirtyLevels.Dirty) {
90100
break
@@ -296,13 +306,27 @@ export function triggerEffects(
296306
) {
297307
pauseScheduling()
298308
for (const effect of dep.keys()) {
309+
if (!dep.computed && effect.computed) {
310+
if (dep.get(effect) === effect._trackId && effect._runnings > 0) {
311+
effect._dirtyLevel = DirtyLevels.MaybeDirty_ComputedSideEffect_Origin
312+
continue
313+
}
314+
}
299315
// dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
300316
let tracking: boolean | undefined
301317
if (
302318
effect._dirtyLevel < dirtyLevel &&
303319
(tracking ??= dep.get(effect) === effect._trackId)
304320
) {
305321
effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty
322+
// always schedule if the computed is original side effect
323+
// since we know it is actually dirty
324+
if (
325+
effect.computed &&
326+
effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect_Origin
327+
) {
328+
effect._shouldSchedule = true
329+
}
306330
effect._dirtyLevel = dirtyLevel
307331
}
308332
if (

0 commit comments

Comments
 (0)
Please sign in to comment.