在 setupContext 中提供 emit 并触发父组件传入的事件

2025-10-29 11:35:33

上一节实现了组件 props 的更新。

除了接收 props,子组件还需要能够向父组件触发事件。

import { createApp, h } from '../dist/vue.esm.js'

const Child = {
  setup(props, { emit }) {
    return () => {
      return h(
        'button',
        {
          onClick() {
            emit('fn', 1, 2, 3, 4)
          }
        },
        'child'
      )
    }
  }
}

const Com = {
  setup() {
    return () => {
      return h(Child, {
        onFn(a, b, c, d) {
          console.log('fn', a, b, c, d)
        }
      })
    }
  }
}

createApp(Com, { msg: '消息' }).mount('#app')

点击子组件中的按钮时,会执行:

emit('fn', 1, 2, 3, 4)

最终触发父组件传入的 onFn:

onFn(a, b, c, d)
{
  console.log('fn', a, b, c, d)
}

组件事件的触发流程为:

子组件调用 emit('fn')
  -> 将事件名 fn 转换成 onFn
  -> 从组件 vnode.props 中取出 onFn
  -> 执行

实现

1. 在 setup 中提供 emit

emit 是通过 setup 的第二个参数传进去的。

之前的 setupContext 只提供了 attrs:

function createSetupContext(instance) {
  return {
    attrs: instance.attrs
  }
}

现在需要在 setupContext 中增加 emit:

function createSetupContext(instance) {
  return {
    get attrs() {
      return instance.attrs
    },
    emit(event, ...args) {
      emit(instance, event, ...args)
    }
  }
}
  1. attrs 使用 get,每次访问时都从当前实例上读取最新的 attrs
  2. emit 内部需要拿到组件实例,所以这里用闭包把 instance 传进去

这样,用户在 setup 中就可以通过第二个参数拿到 emit:

setup(props, { emit })
{
  emit('fn')
}

2. 实现 emit

拿到事件属性名后,就可以从 instance.vnode.props 中读取对应的事件处理函数。

import { isFunction } from '@vue/shared'

function emit(instance, event, ...args) {
  // 转换事件名
  const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
  const handler = instance.vnode.props?.[eventName]

  if (isFunction(handler)) {
    handler(...args)
  }
}

emit 中只需要关心三件事:

  1. 根据事件名生成事件属性名
  2. 从 vnode.props 中取出事件处理函数
  3. 执行事件处理函数并透传参数
// packages/runtime-core/src/componentEmits.ts
import { isFunction, toHandlerKey } from '@vue/shared'

export function emit(instance, event, ...args) {
  const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
  const handler = instance.vnode.props?.[eventName]

  if (isFunction(handler)) {
    handler(...args)
  }
}

最后在创建 setupContext 时引入这个 emit:

// packages/runtime-core/src/component.ts
import { emit } from './componentEmits'

function createSetupContext(instance) {
  return {
    get attrs() {
      return instance.attrs
    },
    emit(event, ...args) {
      emit(instance, event, ...args)
    }
  }
}

这样,组件就可以通过 setup(props, { emit }) 触发父组件传入的事件。

区别在于:

  • 普通 props 用来给子组件传递数据
  • onXxx 形式的 props 用来给子组件传递事件处理函数