diff --git a/src/core/instance/lifecycle.ts b/src/core/instance/lifecycle.ts index 0d1f0732087..df70b7113fd 100644 --- a/src/core/instance/lifecycle.ts +++ b/src/core/instance/lifecycle.ts @@ -18,7 +18,7 @@ import { invokeWithErrorHandling } from '../util/index' import { currentInstance, setCurrentInstance } from 'v3/currentInstance' -import { syncSetupAttrs } from 'v3/apiSetup' +import { syncSetupProxy } from 'v3/apiSetup' export let activeInstance: any = null export let isUpdatingChildComponent: boolean = false @@ -288,11 +288,12 @@ export function updateChildComponent( // force update if attrs are accessed and has changed since it may be // passed to a child component. if ( - syncSetupAttrs( + syncSetupProxy( vm._attrsProxy, attrs, (prevVNode.data && prevVNode.data.attrs) || emptyObject, - vm + vm, + '$attrs' ) ) { needsForceUpdate = true @@ -300,7 +301,20 @@ export function updateChildComponent( } vm.$attrs = attrs - vm.$listeners = listeners || emptyObject + // update listeners + listeners = listeners || emptyObject + const prevListeners = vm.$options._parentListeners + if (vm._listenersProxy) { + syncSetupProxy( + vm._listenersProxy, + listeners, + prevListeners || emptyObject, + vm, + '$listeners' + ) + } + vm.$listeners = vm.$options._parentListeners = listeners + updateComponentListeners(vm, listeners, prevListeners) // update props if (propsData && vm.$options.props) { @@ -317,12 +331,6 @@ export function updateChildComponent( vm.$options.propsData = propsData } - // update listeners - listeners = listeners || emptyObject - const oldListeners = vm.$options._parentListeners - vm.$options._parentListeners = listeners - updateComponentListeners(vm, listeners, oldListeners) - // resolve slots + force update if has children if (needsForceUpdate) { vm.$slots = resolveSlots(renderChildren, parentVnode.context) diff --git a/src/types/component.ts b/src/types/component.ts index 23a4adb86e3..5d16fa15f7a 100644 --- a/src/types/component.ts +++ b/src/types/component.ts @@ -111,6 +111,7 @@ export declare class Component { _setupProxy?: Record _setupContext?: SetupContext _attrsProxy?: Record + _listenersProxy?: Record _slotsProxy?: Record VNode[]> _preWatchers?: Watcher[] diff --git a/src/v3/apiSetup.ts b/src/v3/apiSetup.ts index e0573586413..ea8448072c2 100644 --- a/src/v3/apiSetup.ts +++ b/src/v3/apiSetup.ts @@ -19,6 +19,7 @@ import { proxyWithRefUnwrap } from './reactivity/ref' */ export interface SetupContext { attrs: Record + listeners: Record slots: Record VNode[]> emit: (event: string, ...args: any[]) => any expose: (exposed: Record) => void @@ -87,7 +88,19 @@ function createSetupContext(vm: Component): SetupContext { let exposeCalled = false return { get attrs() { - return initAttrsProxy(vm) + if (!vm._attrsProxy) { + const proxy = (vm._attrsProxy = {}) + def(proxy, '_v_attr_proxy', true) + syncSetupProxy(proxy, vm.$attrs, emptyObject, vm, '$attrs') + } + return vm._attrsProxy + }, + get listeners() { + if (!vm._listenersProxy) { + const proxy = (vm._listenersProxy = {}) + syncSetupProxy(proxy, vm.$listeners, emptyObject, vm, '$listeners') + } + return vm._listenersProxy }, get slots() { return initSlotsProxy(vm) @@ -109,26 +122,18 @@ function createSetupContext(vm: Component): SetupContext { } } -function initAttrsProxy(vm: Component) { - if (!vm._attrsProxy) { - const proxy = (vm._attrsProxy = {}) - def(proxy, '_v_attr_proxy', true) - syncSetupAttrs(proxy, vm.$attrs, emptyObject, vm) - } - return vm._attrsProxy -} - -export function syncSetupAttrs( +export function syncSetupProxy( to: any, from: any, prev: any, - instance: Component + instance: Component, + type: string ) { let changed = false for (const key in from) { if (!(key in to)) { changed = true - defineProxyAttr(to, key, instance) + defineProxyAttr(to, key, instance, type) } else if (from[key] !== prev[key]) { changed = true } @@ -142,12 +147,17 @@ export function syncSetupAttrs( return changed } -function defineProxyAttr(proxy: any, key: string, instance: Component) { +function defineProxyAttr( + proxy: any, + key: string, + instance: Component, + type: string +) { Object.defineProperty(proxy, key, { enumerable: true, configurable: true, get() { - return instance.$attrs[key] + return instance[type][key] } }) } @@ -171,19 +181,23 @@ export function syncSetupSlots(to: any, from: any) { } /** - * @internal use manual type def + * @internal use manual type def because it relies on legacy VNode types */ export function useSlots(): SetupContext['slots'] { return getContext().slots } -/** - * @internal use manual type def - */ export function useAttrs(): SetupContext['attrs'] { return getContext().attrs } +/** + * Vue 2 only + */ +export function useListeners(): SetupContext['listeners'] { + return getContext().listeners +} + function getContext(): SetupContext { if (__DEV__ && !currentInstance) { warn(`useContext() called without active instance.`) diff --git a/src/v3/index.ts b/src/v3/index.ts index 30b89c3162e..90dfacc17ed 100644 --- a/src/v3/index.ts +++ b/src/v3/index.ts @@ -77,7 +77,7 @@ export { provide, inject, InjectionKey } from './apiInject' export { h } from './h' export { getCurrentInstance } from './currentInstance' -export { useSlots, useAttrs, mergeDefaults } from './apiSetup' +export { useSlots, useAttrs, useListeners, mergeDefaults } from './apiSetup' export { nextTick } from 'core/util/next-tick' export { set, del } from 'core/observer' diff --git a/test/unit/features/v3/apiSetup.spec.ts b/test/unit/features/v3/apiSetup.spec.ts index 4ae9e739e91..7c69d06d1c4 100644 --- a/test/unit/features/v3/apiSetup.spec.ts +++ b/test/unit/features/v3/apiSetup.spec.ts @@ -297,4 +297,27 @@ describe('api: setup context', () => { await nextTick() expect(spy).toHaveBeenCalledTimes(1) }) + + it('context.listeners', async () => { + let _listeners + const Child = { + setup(_, { listeners }) { + _listeners = listeners + return () => {} + } + } + + const Parent = { + data: () => ({ log: () => 1 }), + template: ``, + components: { Child } + } + + const vm = new Vue(Parent).$mount() + + expect(_listeners.foo()).toBe(1) + vm.log = () => 2 + await nextTick() + expect(_listeners.foo()).toBe(2) + }) }) diff --git a/types/v3-manual-apis.d.ts b/types/v3-manual-apis.d.ts index 901494d624d..2f8e94b113c 100644 --- a/types/v3-manual-apis.d.ts +++ b/types/v3-manual-apis.d.ts @@ -5,6 +5,4 @@ export function getCurrentInstance(): { proxy: Vue } | null export const h: CreateElement -export function useAttrs(): SetupContext['attrs'] - export function useSlots(): SetupContext['slots'] diff --git a/types/v3-setup-context.d.ts b/types/v3-setup-context.d.ts index 7e11fe095a4..77b49bed8a6 100644 --- a/types/v3-setup-context.d.ts +++ b/types/v3-setup-context.d.ts @@ -31,6 +31,10 @@ export type EmitFn< export interface SetupContext { attrs: Data + /** + * Equivalent of `this.$listeners`, which is Vue 2 only. + */ + listeners: Record slots: Slots emit: EmitFn expose(exposed?: Record): void