diff --git a/packages/reactivity/__tests__/computed.spec.ts b/packages/reactivity/__tests__/computed.spec.ts index 9d60ad352c4..3772ff41b9f 100644 --- a/packages/reactivity/__tests__/computed.spec.ts +++ b/packages/reactivity/__tests__/computed.spec.ts @@ -7,6 +7,7 @@ import { WritableComputedRef, isReadonly } from '../src' +import { isComputed } from '../src/computed' describe('reactivity/computed', () => { it('should return updated value', () => { @@ -193,4 +194,27 @@ describe('reactivity/computed', () => { expect(isReadonly(z)).toBe(false) expect(isReadonly(z.value.a)).toBe(false) }) + + it('isComputed', () => { + expect(isComputed(computed(() => 1))).toBe(true) + expect( + isComputed( + computed({ + get: () => 1, + set: () => undefined + }) + ) + ).toBe(true) + + expect(isComputed(ref(1))).toBe(false) + expect(isComputed(0)).toBe(false) + expect(isComputed(1)).toBe(false) + // an object that looks like a computed ref isn't necessarily a computed ref + expect( + isComputed({ + value: 0, + effect: () => undefined + }) + ).toBe(false) + }) }) diff --git a/packages/reactivity/src/computed.ts b/packages/reactivity/src/computed.ts index 7f2c5b50d80..e9ca74a19bf 100644 --- a/packages/reactivity/src/computed.ts +++ b/packages/reactivity/src/computed.ts @@ -89,3 +89,8 @@ export function computed( isFunction(getterOrOptions) || !getterOrOptions.set ) as any } + +export function isComputed(r: ComputedRef | unknown): r is ComputedRef +export function isComputed(r: any): r is ComputedRef { + return Boolean(r && r instanceof ComputedRefImpl) +} diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 31bca6bed3f..108269f8acf 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -785,4 +785,17 @@ describe('api: watch', () => { await nextTick() expect(spy).toHaveBeenCalledTimes(1) }) + + // #2231 + test('computed refs should not trigger watch if value has no change', async () => { + const spy = jest.fn() + const source = ref(0) + const price = computed(() => source.value === 0) + watch(price, spy) + source.value++ + await nextTick() + source.value++ + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 14253a2a403..fdee63e2a40 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -33,6 +33,7 @@ import { } from './errorHandling' import { queuePostRenderEffect } from './renderer' import { warn } from './warning' +import { isComputed } from '../../reactivity/src/computed' export type WatchEffect = (onInvalidate: InvalidateCbRegistrator) => void @@ -161,9 +162,12 @@ function doWatch( } let getter: () => any - const isRefSource = isRef(source) - if (isRefSource) { + // Skip value comparison only for non-computed refs. because computed refs always + // trigger SET no matter whether the value has changed or not. #2231 + let skipCompare = false + if (isRef(source)) { getter = () => (source as Ref).value + skipCompare = !isComputed(source) } else if (isReactive(source)) { getter = () => source deep = true @@ -242,7 +246,7 @@ function doWatch( if (cb) { // watch(source, cb) const newValue = runner() - if (deep || isRefSource || hasChanged(newValue, oldValue)) { + if (deep || skipCompare || hasChanged(newValue, oldValue)) { // cleanup before running cb again if (cleanup) { cleanup()