]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(dx): warn users when computed is self-triggering (#10299)
authorDoctor Wu <44631608+Doctor-wu@users.noreply.github.com>
Tue, 13 Feb 2024 09:38:26 +0000 (17:38 +0800)
committerGitHub <noreply@github.com>
Tue, 13 Feb 2024 09:38:26 +0000 (17:38 +0800)
packages/reactivity/__tests__/computed.spec.ts
packages/reactivity/src/computed.ts

index c3d0c7f15eda61710c6314a0e7140e00fe8bd669..c9f47720eddc429f9f273024a8b1187fee63a1cd 100644 (file)
@@ -14,6 +14,7 @@ import {
   toRaw,
 } from '../src'
 import { DirtyLevels } from '../src/constants'
+import { COMPUTED_SIDE_EFFECT_WARN } from '../src/computed'
 
 describe('reactivity/computed', () => {
   it('should return updated value', () => {
@@ -488,6 +489,7 @@ describe('reactivity/computed', () => {
     expect(c3.effect._dirtyLevel).toBe(
       DirtyLevels.MaybeDirty_ComputedSideEffect,
     )
+    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
   it('should work when chained(ref+computed)', () => {
@@ -502,6 +504,7 @@ describe('reactivity/computed', () => {
     expect(c2.value).toBe('0foo')
     expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
     expect(c2.value).toBe('1foo')
+    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
   it('should trigger effect even computed already dirty', () => {
@@ -524,6 +527,7 @@ describe('reactivity/computed', () => {
     expect(c2.effect._dirtyLevel).toBe(DirtyLevels.Dirty)
     v.value = 2
     expect(fnSpy).toBeCalledTimes(2)
+    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
   // #10185
@@ -567,6 +571,7 @@ describe('reactivity/computed', () => {
     expect(c3.effect._dirtyLevel).toBe(DirtyLevels.MaybeDirty)
 
     expect(c3.value).toBe('yes')
+    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
   it('should be not dirty after deps mutate (mutate deps in computed)', async () => {
@@ -588,6 +593,7 @@ describe('reactivity/computed', () => {
     await nextTick()
     await nextTick()
     expect(serializeInner(root)).toBe(`2`)
+    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 
   it('should not trigger effect scheduler by recurse computed effect', async () => {
@@ -610,5 +616,6 @@ describe('reactivity/computed', () => {
     v.value += ' World'
     await nextTick()
     expect(serializeInner(root)).toBe('Hello World World World World')
+    expect(COMPUTED_SIDE_EFFECT_WARN).toHaveBeenWarned()
   })
 })
index 259d4e32c8a1c79bea5d23949e29b593fcf427d8..a4b74172fcfd98a929ba5eb212db3aade8ad604d 100644 (file)
@@ -4,6 +4,7 @@ import { NOOP, hasChanged, isFunction } from '@vue/shared'
 import { toRaw } from './reactive'
 import type { Dep } from './dep'
 import { DirtyLevels, ReactiveFlags } from './constants'
+import { warn } from './warning'
 
 declare const ComputedRefSymbol: unique symbol
 
@@ -24,6 +25,12 @@ export interface WritableComputedOptions<T> {
   set: ComputedSetter<T>
 }
 
+export const COMPUTED_SIDE_EFFECT_WARN =
+  `Computed is still dirty after getter evaluation,` +
+  ` likely because a computed is mutating its own dependency in its getter.` +
+  ` State mutations in computed getters should be avoided. ` +
+  ` Check the docs for more details: https://vuejs.org/guide/essentials/computed.html#getters-should-be-side-effect-free`
+
 export class ComputedRefImpl<T> {
   public dep?: Dep = undefined
 
@@ -67,6 +74,7 @@ export class ComputedRefImpl<T> {
     }
     trackRefValue(self)
     if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
+      __DEV__ && warn(COMPUTED_SIDE_EFFECT_WARN)
       triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
     }
     return self._value
@@ -141,7 +149,7 @@ export function computed<T>(
     getter = getterOrOptions
     setter = __DEV__
       ? () => {
-          console.warn('Write operation failed: computed value is readonly')
+          warn('Write operation failed: computed value is readonly')
         }
       : NOOP
   } else {