diff --git a/packages/runtime-core/__tests__/apiOptions.spec.ts b/packages/runtime-core/__tests__/apiOptions.spec.ts index 87d57afc91d..10ede8a4638 100644 --- a/packages/runtime-core/__tests__/apiOptions.spec.ts +++ b/packages/runtime-core/__tests__/apiOptions.spec.ts @@ -279,6 +279,67 @@ describe('api: options', () => { assertCall(spyC, 0, [{ qux: 4 }, { qux: 4 }]) }) + // #3966 + test('watch merging from mixins', async () => { + const mixinA = { + data() { + return { + fromMixinA: '' + } + }, + watch: { + obj: { + handler(this: any, to: any) { + this.fromMixinA = to + } + } + } + } + + const mixinB = { + data() { + return { + fromMixinB: '' + } + }, + watch: { + obj: 'setMixinB' + }, + methods: { + setMixinB(this: any, to: any) { + this.fromMixinB = to + } + } + } + + let vm: any + const Comp = { + render() {}, + mixins: [mixinA, mixinB], + data: () => ({ + obj: 'foo', + fromComp: '' + }), + watch: { + obj(this: any, to: any) { + this.fromComp = to + } + }, + mounted() { + vm = this + } + } + + const root = nodeOps.createElement('div') + render(h(Comp), root) + + vm.obj = 'bar' + await nextTick() + expect(vm.fromComp).toBe('bar') + expect(vm.fromMixinA).toBe('bar') + expect(vm.fromMixinB).toBe('bar') + }) + test('provide/inject', () => { const symbolKey = Symbol() const Root = defineComponent({ diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 11c42fcd8e0..c6398378ec8 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -972,25 +972,23 @@ export const internalOptionMergeStrats: Record = { methods: mergeObjectOptions, computed: mergeObjectOptions, // lifecycle - beforeCreate: mergeHook, - created: mergeHook, - beforeMount: mergeHook, - mounted: mergeHook, - beforeUpdate: mergeHook, - updated: mergeHook, - beforeDestroy: mergeHook, - destroyed: mergeHook, - activated: mergeHook, - deactivated: mergeHook, - errorCaptured: mergeHook, - serverPrefetch: mergeHook, + beforeCreate: mergeAsArray, + created: mergeAsArray, + beforeMount: mergeAsArray, + mounted: mergeAsArray, + beforeUpdate: mergeAsArray, + updated: mergeAsArray, + beforeDestroy: mergeAsArray, + destroyed: mergeAsArray, + activated: mergeAsArray, + deactivated: mergeAsArray, + errorCaptured: mergeAsArray, + serverPrefetch: mergeAsArray, // assets components: mergeObjectOptions, directives: mergeObjectOptions, - // watch has special merge behavior in v2, but isn't actually needed in v3. - // since we are only exposing these for compat and nobody should be relying - // on the watch-specific behavior, just expose the object merge strat. - watch: mergeObjectOptions, + // watch + watch: mergeWatchOptions, // provide / inject provide: mergeDataFn, inject: mergeInject @@ -1038,13 +1036,23 @@ function normalizeInject( return raw } -function mergeHook( - to: Function[] | Function | undefined, - from: Function | Function[] -) { +function mergeAsArray(to: T[] | T | undefined, from: T | T[]) { return to ? [...new Set([].concat(to as any, from as any))] : from } function mergeObjectOptions(to: Object | undefined, from: Object | undefined) { return to ? extend(extend(Object.create(null), to), from) : from } + +function mergeWatchOptions( + to: ComponentWatchOptions | undefined, + from: ComponentWatchOptions | undefined +) { + if (!to) return from + if (!from) return to + const merged = extend(Object.create(null), to) + for (const key in from) { + merged[key] = mergeAsArray(to[key], from[key]) + } + return merged +}