]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(reactivity): avoid infinite recursion from side effects in computed getter (...
authorJohnson Chu <johnsoncodehk@gmail.com>
Tue, 6 Feb 2024 10:44:09 +0000 (18:44 +0800)
committerGitHub <noreply@github.com>
Tue, 6 Feb 2024 10:44:09 +0000 (18:44 +0800)
close #10214

packages/reactivity/__tests__/computed.spec.ts
packages/reactivity/src/computed.ts
packages/reactivity/src/constants.ts
packages/reactivity/src/effect.ts
packages/reactivity/src/ref.ts

index 5a0beb973e854204bd55a931c2f7a1f90ba90110..c3d0c7f15eda61710c6314a0e7140e00fe8bd669 100644 (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')
+  })
 })
index 9eed5bc8399a924afa8176558a3b4b8c52553f64..259d4e32c8a1c79bea5d23949e29b593fcf427d8 100644 (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
   }
index 5e9716dd88e29abf66091bb0cfeaba78191c7983..baa75d61644c34ec76861948dab497a141ee243a 100644 (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,
 }
index 91d9105af20ec01688886b4ad2332c1a77f11ce0..ca90544c0deab7ddd29a441137b2c6656d57fb32 100644 (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)
index a3fdde4836626a13c2d518ca7e210dd267648957..1b9d60ef06b62a2b78c19e7f02b71e203b932c0c 100644 (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,