2026.01.27
组件事件机制 -> emit 方法的实现与组件通信
在之前的章节中,我们实现了通过父组件的 Props 传递,在更新的同时通知子组件更新。除了这种方式之外,子组件也可以通过触发事件来通知父组件,这正是 Vue 中经典的"子传父"通信模式。
在之前的章节中,我们实现了通过父组件的 Props 传递,在更新的同时通知子组件更新。除了这种方式之外,子组件也可以通过触发事件来通知父组件,这正是 Vue 中经典的"子传父"通信模式。
问题场景
让我们先通过一个示例来观察组件事件的场景:
<div id="app"></div>
<script type="module">
import { h, createApp, ref } from '../dist/vue.esm.js'
const Child = {
props: ['age'],
setup(props, { emit }) {
return () => {
return h('button', {
onClick() {
emit('foo', 1, 2, 3)
}
}, 'child component')
}
}
}
const Comp = {
setup() {
return () => {
return h('div', [
h('p', 'p'),
h(Child, {
onFoo(...args) {
console.log('父组件传递的事件', args)
}
})
])
}
}
}
createApp(Comp).mount('#app')
</script>
核心问题:
- 子组件通过
emit('foo', 1, 2, 3)触发事件 - 父组件通过
onFoo监听事件并接收参数 - 目前
emit功能还未实现,需要我们来完成
实现 emit
1. 事件名转换规则
Vue 的事件处理采用如下命名约定:
- 事件触发:使用 kebab-case,如
emit('foo') - 事件监听:使用
on前缀 + 首字母大写,如onFoo
因此,我们需要将事件名转换为对应的属性名:
/**
* 将事件名转换为对应的属性名
* @example 'foo' -> 'onFoo'
*/
function toEventName(event) {
return `on${event[0].toUpperCase() + event.slice(1)}`
}
2. 基础 emit 实现
首先在 createSetupContext 中添加 emit 方法:
/**
* 创建 setup 函数的上下文
*/
function createSetupContext(instance) {
return {
get attrs() {
return instance.attrs
},
/**
* 触发组件事件
* @param event 事件名
* @param args 传递给事件处理函数的参数
*/
emit(event, ...args) {
const eventName = toEventName(event)
/**
* 从 vnode.props 中获取对应的事件处理函数
* 父组件通过 h(Child, { onFoo: handler }) 的方式传递事件
*/
const handler = instance.vnode.props[eventName]
if (isFunction(handler)) {
handler(...args)
}
}
}
}
设计要点:
instance.vnode.props:存储父组件传递的所有属性,包括事件处理函数- 事件处理函数通过
onFoo的形式存在,需要先进行名称转换 - 只有当
handler是函数时才调用,避免错误
3. 抽离 emit 函数
为了兼容 Vue 2.x 的 $emit API,我们需要将 emit 逻辑抽离成独立函数,并挂载到组件实例上:
/**
* 触发组件事件的核心实现
* @param instance 组件实例
* @param event 事件名
* @param args 传递给事件处理函数的参数
*/
function emit(instance, event, ...args) {
const eventName = toEventName(event)
const handler = instance.vnode.props[eventName]
if (isFunction(handler)) {
handler(...args)
}
}
/**
* 创建 setup 函数的上下文
*/
function createSetupContext(instance) {
return {
get attrs() {
return instance.attrs
},
emit(event, ...args) {
emit(instance, event, ...args)
}
}
}
设计要点:
emit函数独立于createSetupContext,便于在多处复用- setup 中的
emit方法内部调用核心的emit函数 - 保持了代码的解耦和可维护性
4. 挂载到组件实例
为了支持组件实例访问 $emit,我们需要在创建组件实例时将 emit 挂载到实例上:
/**
* 创建组件实例
*/
export function createComponentInstance(vnode) {
const { type } = vnode
const instance: any = {}
instance.ctx = { _: instance }
/**
* 将 emit 挂载到实例上,供内部使用
*/
instance.emit = (event, ...args) => emit(instance, event, ...args)
return instance
}
5. 添加到 publicPropertiesMap
最后,将 $emit 添加到公共属性映射表,使其可以通过组件实例代理访问:
/**
* 组件实例的公共属性映射
* 通过代理机制,使组件实例可以访问这些属性
*/
const publicPropertiesMap = {
$el: instance => instance.vnode.el,
$attrs: instance => instance.attrs,
$slots: instance => instance.slots,
$refs: instance => instance.refs,
$nextTick: (instance) => {
return nextTick.bind(instance)
},
$forceUpdate: (instance) => {
return () => instance.update()
},
/**
* 暴露 $emit API
* 在 Options API 中可以通过 this.$emit 触发事件
*/
$emit: instance => instance.emit
}
总结
至此,我们完成了组件事件机制的完整实现:
1. 事件命名约定
- 触发事件:kebab-case 形式,如
emit('foo') - 监听事件:
on前缀 + 首字母大写,如onFoo
2. 核心实现
emit函数:事件触发的核心逻辑,从vnode.props中获取并调用事件处理函数createSetupContext:在 setup 中通过解构{ emit }获取事件触发方法
3. 兼容性支持
- 将
emit挂载到组件实例instance.emit - 通过
publicPropertiesMap暴露$emit,支持 Options API
4. 工作流程
- 父组件通过
h(Child, { onFoo: handler })传递事件处理函数 - 处理函数存储在子组件的
vnode.props.onFoo中 - 子组件通过
emit('foo', ...args)触发事件 emit将foo转换为onFoo,从 props 中获取并调用处理函数
这套机制实现了子组件向父组件的通信,是 Vue 组件通信体系的重要组成部分,与 Props 传递形成完整的父子组件通信方案。