feat(setup): support listeners on setup context + useListeners() helper

These are added because Vue 2 does not include listeners in
`context.attrs` so there is no way to access the equivalent of
`this.$listeners` in `setup()`.
This commit is contained in:
Evan You 2022-07-20 12:13:04 +08:00
parent 135d07442a
commit adf3ac8adc
7 changed files with 80 additions and 32 deletions

View File

@ -18,7 +18,7 @@ import {
invokeWithErrorHandling invokeWithErrorHandling
} from '../util/index' } from '../util/index'
import { currentInstance, setCurrentInstance } from 'v3/currentInstance' import { currentInstance, setCurrentInstance } from 'v3/currentInstance'
import { syncSetupAttrs } from 'v3/apiSetup' import { syncSetupProxy } from 'v3/apiSetup'
export let activeInstance: any = null export let activeInstance: any = null
export let isUpdatingChildComponent: boolean = false 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 // force update if attrs are accessed and has changed since it may be
// passed to a child component. // passed to a child component.
if ( if (
syncSetupAttrs( syncSetupProxy(
vm._attrsProxy, vm._attrsProxy,
attrs, attrs,
(prevVNode.data && prevVNode.data.attrs) || emptyObject, (prevVNode.data && prevVNode.data.attrs) || emptyObject,
vm vm,
'$attrs'
) )
) { ) {
needsForceUpdate = true needsForceUpdate = true
@ -300,7 +301,20 @@ export function updateChildComponent(
} }
vm.$attrs = attrs 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 // update props
if (propsData && vm.$options.props) { if (propsData && vm.$options.props) {
@ -317,12 +331,6 @@ export function updateChildComponent(
vm.$options.propsData = propsData 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 // resolve slots + force update if has children
if (needsForceUpdate) { if (needsForceUpdate) {
vm.$slots = resolveSlots(renderChildren, parentVnode.context) vm.$slots = resolveSlots(renderChildren, parentVnode.context)

View File

@ -111,6 +111,7 @@ export declare class Component {
_setupProxy?: Record<string, any> _setupProxy?: Record<string, any>
_setupContext?: SetupContext _setupContext?: SetupContext
_attrsProxy?: Record<string, any> _attrsProxy?: Record<string, any>
_listenersProxy?: Record<string, Function | Function[]>
_slotsProxy?: Record<string, () => VNode[]> _slotsProxy?: Record<string, () => VNode[]>
_preWatchers?: Watcher[] _preWatchers?: Watcher[]

View File

@ -19,6 +19,7 @@ import { proxyWithRefUnwrap } from './reactivity/ref'
*/ */
export interface SetupContext { export interface SetupContext {
attrs: Record<string, any> attrs: Record<string, any>
listeners: Record<string, Function | Function[]>
slots: Record<string, () => VNode[]> slots: Record<string, () => VNode[]>
emit: (event: string, ...args: any[]) => any emit: (event: string, ...args: any[]) => any
expose: (exposed: Record<string, any>) => void expose: (exposed: Record<string, any>) => void
@ -87,7 +88,19 @@ function createSetupContext(vm: Component): SetupContext {
let exposeCalled = false let exposeCalled = false
return { return {
get attrs() { 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() { get slots() {
return initSlotsProxy(vm) return initSlotsProxy(vm)
@ -109,26 +122,18 @@ function createSetupContext(vm: Component): SetupContext {
} }
} }
function initAttrsProxy(vm: Component) { export function syncSetupProxy(
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(
to: any, to: any,
from: any, from: any,
prev: any, prev: any,
instance: Component instance: Component,
type: string
) { ) {
let changed = false let changed = false
for (const key in from) { for (const key in from) {
if (!(key in to)) { if (!(key in to)) {
changed = true changed = true
defineProxyAttr(to, key, instance) defineProxyAttr(to, key, instance, type)
} else if (from[key] !== prev[key]) { } else if (from[key] !== prev[key]) {
changed = true changed = true
} }
@ -142,12 +147,17 @@ export function syncSetupAttrs(
return changed return changed
} }
function defineProxyAttr(proxy: any, key: string, instance: Component) { function defineProxyAttr(
proxy: any,
key: string,
instance: Component,
type: string
) {
Object.defineProperty(proxy, key, { Object.defineProperty(proxy, key, {
enumerable: true, enumerable: true,
configurable: true, configurable: true,
get() { 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'] { export function useSlots(): SetupContext['slots'] {
return getContext().slots return getContext().slots
} }
/**
* @internal use manual type def
*/
export function useAttrs(): SetupContext['attrs'] { export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs return getContext().attrs
} }
/**
* Vue 2 only
*/
export function useListeners(): SetupContext['listeners'] {
return getContext().listeners
}
function getContext(): SetupContext { function getContext(): SetupContext {
if (__DEV__ && !currentInstance) { if (__DEV__ && !currentInstance) {
warn(`useContext() called without active instance.`) warn(`useContext() called without active instance.`)

View File

@ -77,7 +77,7 @@ export { provide, inject, InjectionKey } from './apiInject'
export { h } from './h' export { h } from './h'
export { getCurrentInstance } from './currentInstance' 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 { nextTick } from 'core/util/next-tick'
export { set, del } from 'core/observer' export { set, del } from 'core/observer'

View File

@ -297,4 +297,27 @@ describe('api: setup context', () => {
await nextTick() await nextTick()
expect(spy).toHaveBeenCalledTimes(1) expect(spy).toHaveBeenCalledTimes(1)
}) })
it('context.listeners', async () => {
let _listeners
const Child = {
setup(_, { listeners }) {
_listeners = listeners
return () => {}
}
}
const Parent = {
data: () => ({ log: () => 1 }),
template: `<Child @foo="log" />`,
components: { Child }
}
const vm = new Vue(Parent).$mount()
expect(_listeners.foo()).toBe(1)
vm.log = () => 2
await nextTick()
expect(_listeners.foo()).toBe(2)
})
}) })

View File

@ -5,6 +5,4 @@ export function getCurrentInstance(): { proxy: Vue } | null
export const h: CreateElement export const h: CreateElement
export function useAttrs(): SetupContext['attrs']
export function useSlots(): SetupContext['slots'] export function useSlots(): SetupContext['slots']

View File

@ -31,6 +31,10 @@ export type EmitFn<
export interface SetupContext<E extends EmitsOptions = {}> { export interface SetupContext<E extends EmitsOptions = {}> {
attrs: Data attrs: Data
/**
* Equivalent of `this.$listeners`, which is Vue 2 only.
*/
listeners: Record<string, Function | Function[]>
slots: Slots slots: Slots
emit: EmitFn<E> emit: EmitFn<E>
expose(exposed?: Record<string, any>): void expose(exposed?: Record<string, any>): void