Przeglądaj źródła

feat(dx): warn users when computed is self-triggering (#10299)

Doctor Wu 2 lat temu
rodzic
commit
f7ba97f975

+ 7 - 0
packages/reactivity/__tests__/computed.spec.ts

@@ -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()
   })
 })

+ 9 - 1
packages/reactivity/src/computed.ts

@@ -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 {