mirror of
https://gitee.com/vuejs/vue.git
synced 2024-11-29 18:47:39 +08:00
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:
parent
135d07442a
commit
adf3ac8adc
@ -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)
|
||||
|
@ -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[]
|
||||
|
||||
|
@ -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.`)
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
2
types/v3-manual-apis.d.ts
vendored
2
types/v3-manual-apis.d.ts
vendored
@ -5,6 +5,4 @@ export function getCurrentInstance(): { proxy: Vue } | null
|
||||
|
||||
export const h: CreateElement
|
||||
|
||||
export function useAttrs(): SetupContext['attrs']
|
||||
|
||||
export function useSlots(): SetupContext['slots']
|
||||
|
4
types/v3-setup-context.d.ts
vendored
4
types/v3-setup-context.d.ts
vendored
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user