diff --git a/packages/reactivity/src/ref.ts b/packages/reactivity/src/ref.ts index 6596ecdd..04464fdf 100644 --- a/packages/reactivity/src/ref.ts +++ b/packages/reactivity/src/ref.ts @@ -7,13 +7,17 @@ import { CollectionTypes } from './collectionHandlers' declare const RefSymbol: unique symbol export interface Ref { + value: T /** * Type differentiator only. * We need this to be in public d.ts but don't want it to show up in IDE * autocomplete, so we use a private Symbol instead. */ [RefSymbol]: true - value: T + /** + * @internal + */ + _shallow?: boolean } export type ToRefs = { [K in keyof T]: Ref } @@ -49,7 +53,7 @@ class RefImpl { public readonly __v_isRef = true - constructor(private _rawValue: T, private readonly _shallow = false) { + constructor(private _rawValue: T, public readonly _shallow = false) { this._value = _shallow ? _rawValue : convert(_rawValue) } @@ -75,7 +79,7 @@ function createRef(rawValue: unknown, shallow = false) { } export function triggerRef(ref: Ref) { - trigger(ref, TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0) + trigger(toRaw(ref), TriggerOpTypes.SET, 'value', __DEV__ ? ref.value : void 0) } export function unref(ref: T): T extends Ref ? V : T { diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 31bca6be..c96db41f 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -13,7 +13,8 @@ import { DebuggerEvent, TrackOpTypes, TriggerOpTypes, - triggerRef + triggerRef, + shallowRef } from '@vue/reactivity' // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch @@ -750,8 +751,8 @@ describe('api: watch', () => { expect(calls).toBe(1) }) - test('should force trigger on triggerRef when watching a ref', async () => { - const v = ref({ a: 1 }) + test('should force trigger on triggerRef when watching a shallow ref', async () => { + const v = shallowRef({ a: 1 }) let sideEffect = 0 watch(v, obj => { sideEffect = obj.a @@ -785,4 +786,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 14253a2a..1e5a44f5 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -161,9 +161,10 @@ function doWatch( } let getter: () => any - const isRefSource = isRef(source) - if (isRefSource) { + let forceTrigger = false + if (isRef(source)) { getter = () => (source as Ref).value + forceTrigger = !!(source as Ref)._shallow } else if (isReactive(source)) { getter = () => source deep = true @@ -242,7 +243,7 @@ function doWatch( if (cb) { // watch(source, cb) const newValue = runner() - if (deep || isRefSource || hasChanged(newValue, oldValue)) { + if (deep || forceTrigger || hasChanged(newValue, oldValue)) { // cleanup before running cb again if (cleanup) { cleanup()