From 22f7d96757956ebe0baafe52256aa327908cc51c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=BF=9C=E6=96=B9os?= Date: Fri, 2 Aug 2024 11:38:07 +0800 Subject: [PATCH] feat(watch): support passing number to `deep` option to control the watch depth (#9572) --- .../runtime-core/__tests__/apiWatch.spec.ts | 141 ++++++++++++++++++ packages/runtime-core/src/apiWatch.ts | 27 ++-- 2 files changed, 153 insertions(+), 15 deletions(-) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index f370a98b782..5bf156fd77b 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -1532,6 +1532,147 @@ describe('api: watch', () => { expect(spy2).toHaveBeenCalledTimes(1) }) + it('watching reactive depth', async () => { + const state = reactive({ + a: { + b: { + c: { + d: { + e: 1, + }, + }, + }, + }, + }) + + const cb = vi.fn() + + watch(state, cb, { deep: 2 }) + + state.a.b = { c: { d: { e: 2 } } } + await nextTick() + expect(cb).toHaveBeenCalledTimes(1) + + state.a.b.c = { d: { e: 3 } } + + await nextTick() + expect(cb).toHaveBeenCalledTimes(1) + + state.a.b = { c: { d: { e: 4 } } } + + await nextTick() + expect(cb).toHaveBeenCalledTimes(2) + }) + + it('watching ref depth', async () => { + const state = ref({ + a: { + b: 2, + }, + }) + + const cb = vi.fn() + + watch(state, cb, { deep: 1 }) + + state.value.a.b = 3 + await nextTick() + expect(cb).toHaveBeenCalledTimes(0) + + state.value.a = { b: 3 } + await nextTick() + expect(cb).toHaveBeenCalledTimes(1) + }) + + it('watching array depth', async () => { + const arr = ref([ + { + a: { + b: 2, + }, + }, + { + a: { + b: 3, + }, + }, + ]) + const cb = vi.fn() + watch(arr, cb, { deep: 2 }) + + arr.value[0].a.b = 3 + await nextTick() + expect(cb).toHaveBeenCalledTimes(0) + + arr.value[0].a = { b: 3 } + await nextTick() + expect(cb).toHaveBeenCalledTimes(1) + + arr.value[1].a = { b: 4 } + await nextTick() + expect(cb).toHaveBeenCalledTimes(2) + + arr.value.push({ a: { b: 5 } }) + await nextTick() + expect(cb).toHaveBeenCalledTimes(3) + + arr.value.pop() + await nextTick() + expect(cb).toHaveBeenCalledTimes(4) + }) + + it('shallowReactive', async () => { + const state = shallowReactive({ + msg: ref('hello'), + foo: { + a: ref(1), + b: 2, + }, + bar: 'bar', + }) + + const spy = vi.fn() + + watch(state, spy) + + state.msg.value = 'hi' + await nextTick() + expect(spy).not.toHaveBeenCalled() + + state.bar = 'bar2' + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + + state.foo.a.value++ + state.foo.b++ + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + + state.bar = 'bar3' + await nextTick() + expect(spy).toHaveBeenCalledTimes(2) + }) + it('watching reactive with deep: false', async () => { + const state = reactive({ + foo: { + a: 2, + }, + bar: 'bar', + }) + + const spy = vi.fn() + + watch(state, spy, { deep: false }) + + state.foo.a++ + await nextTick() + expect(spy).toHaveBeenCalledTimes(0) + + state.bar = 'bar2' + await nextTick() + expect(spy).toHaveBeenCalledTimes(1) + }) + test("effect should be removed from scope's effects after it is stopped", () => { const num = ref(0) let unwatch: () => void diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index dddc64e2aa7..fc2c7e4497d 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -73,7 +73,7 @@ export interface WatchOptionsBase extends DebuggerOptions { export interface WatchOptions extends WatchOptionsBase { immediate?: Immediate - deep?: boolean + deep?: boolean | number once?: boolean } @@ -189,14 +189,6 @@ function doWatch( } } - // TODO remove in 3.5 - if (__DEV__ && deep !== void 0 && typeof deep === 'number') { - warn( - `watch() "deep" option with number value will be used as watch depth in future versions. ` + - `Please use a boolean instead to avoid potential breakage.`, - ) - } - if (__DEV__ && !cb) { if (immediate !== undefined) { warn( @@ -228,11 +220,15 @@ function doWatch( } const instance = currentInstance - const reactiveGetter = (source: object) => - deep === true - ? source // traverse will happen in wrapped getter below - : // for deep: false, only traverse root-level properties - traverse(source, deep === false ? 1 : undefined) + const reactiveGetter = (source: object) => { + // traverse will happen in wrapped getter below + if (deep) return source + // for `deep: false | 0` or shallow reactive, only traverse root-level properties + if (isShallow(source) || deep === false || deep === 0) + return traverse(source, 1) + // for `deep: undefined` on a reactive object, deeply traverse all properties + return traverse(source) + } let getter: () => any let forceTrigger = false @@ -300,7 +296,8 @@ function doWatch( if (cb && deep) { const baseGetter = getter - getter = () => traverse(baseGetter()) + const depth = deep === true ? Infinity : deep + getter = () => traverse(baseGetter(), depth) } let cleanup: (() => void) | undefined