diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index c3d0c7f15ed..c9f47720edd 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/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() }) }) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 259d4e32c8a..a4b74172fcf 100644 --- a/packages/reactivity/src/computed.ts +++ b/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 { set: ComputedSetter } +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 { public dep?: Dep = undefined @@ -67,6 +74,7 @@ export class ComputedRefImpl { } 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( getter = getterOrOptions setter = __DEV__ ? () => { - console.warn('Write operation failed: computed value is readonly') + warn('Write operation failed: computed value is readonly') } : NOOP } else {