diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 4a0b7a900c0..0c3719e7364 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -915,4 +915,33 @@ describe('api: watch', () => { // should not track b as dependency of Child expect(updated).toHaveBeenCalledTimes(1) }) + + test('watching keypath', async () => { + const spy = jest.fn() + const Comp = defineComponent({ + render() {}, + data() { + return { + a: { + b: 1 + } + } + }, + watch: { + 'a.b': spy + }, + created(this: any) { + this.$watch('a.b', spy) + }, + mounted(this: any) { + this.a.b++ + } + }) + + const root = nodeOps.createElement('div') + createApp(Comp).mount(root) + + await nextTick() + expect(spy).toHaveBeenCalledTimes(2) + }) }) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 9c791689ed6..9498ced48fb 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -334,11 +334,24 @@ export function instanceWatch( ): WatchStopHandle { const publicThis = this.proxy as any const getter = isString(source) - ? () => publicThis[source] + ? source.includes('.') + ? createPathGetter(publicThis, source) + : () => publicThis[source] : source.bind(publicThis) return doWatch(getter, cb.bind(publicThis), options, this) } +export function createPathGetter(ctx: any, path: string) { + const segments = path.split('.') + return () => { + let cur = ctx + for (let i = 0; i < segments.length && cur; i++) { + cur = cur[segments[i]] + } + return cur + } +} + function traverse(value: unknown, seen: Set = new Set()) { if (!isObject(value) || seen.has(value)) { return value diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index e00206baa00..7b4081afecc 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -20,7 +20,12 @@ import { isPromise } from '@vue/shared' import { computed } from './apiComputed' -import { watch, WatchOptions, WatchCallback } from './apiWatch' +import { + watch, + WatchOptions, + WatchCallback, + createPathGetter +} from './apiWatch' import { provide, inject } from './apiInject' import { onBeforeMount, @@ -939,17 +944,6 @@ function createWatcher( } } -function createPathGetter(ctx: any, path: string) { - const segments = path.split('.') - return () => { - let cur = ctx - for (let i = 0; i < segments.length && cur; i++) { - cur = cur[segments[i]] - } - return cur - } -} - export function resolveMergedOptions( instance: ComponentInternalInstance ): ComponentOptions {