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
} 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)

View File

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

View File

@ -19,6 +19,7 @@ import { proxyWithRefUnwrap } from './reactivity/ref'
*/
export interface SetupContext {
attrs: Record<string, any>
listeners: Record<string, Function | Function[]>
slots: Record<string, () => VNode[]>
emit: (event: string, ...args: any[]) => any
expose: (exposed: Record<string, any>) => 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.`)

View File

@ -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'

View File

@ -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: `<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 function useAttrs(): SetupContext['attrs']
export function useSlots(): SetupContext['slots']

View File

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