Procházet zdrojové kódy

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()`.
Evan You před 3 roky
rodič
revize
adf3ac8adc

+ 18 - 10
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)

+ 1 - 0
src/types/component.ts

@@ -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[]
 

+ 33 - 19
src/v3/apiSetup.ts

@@ -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.`)

+ 1 - 1
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'
 

+ 23 - 0
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: `<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)
+  })
 })

+ 0 - 2
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']

+ 4 - 0
types/v3-setup-context.d.ts

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