12.6 组件事件与 emit 的实现
给子组件写
onChange等于传了一个 props;子组件调用emit('change', ...)时,按同样的命名约定去 props 里取出处理函数并调用。
一、约定:事件名映射到 props 键名
自定义事件 change 会被编译成属性 onChange,和普通 props 一起挂在子组件的 vnode.props 上:
const CompVNode = {
type: MyComponent,
props: {
title: ref('A Big Title'),
other: 'other',
// 自定义事件 change 的处理函数
onChange: (a, b) => { console.log('emit change ', a, b) }
}
}
「
on+ 事件名首字母大写」只是一种约定,框架设计者可以自行决定自定义事件编译成什么样子;约定统一后,编译器和运行时才好配合。
二、resolveProps 放宽:on 开头的属性也算 props
for (const key in propsData) {
// 以字符串 on 开头的属性,无论是否在 props 选项中声明,都归入 props,而不是 attrs
if (key in options || key.startsWith('on')) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
事件处理函数并不是子组件「声明接收的数据」,但必须放进 props —— 否则
emit就找不到它。
三、emit 的实现
/**
* 定义 emit 函数,用于发射自定义事件
* @param {string} event 事件名称,如 change
* @param {...any} payload 传递给事件处理函数的参数
*/
function emit(event, ...payload) {
// 按约定处理事件名称:change → onChange
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`
// 去 props 中寻找对应的事件处理函数
const handler = instance.props[eventName]
if (handler) {
handler(...payload)
} else {
console.error('事件不存在')
}
}
// 放进 setupContext,用户通过 setup(props, { emit }) 取用
const setupContext = { attrs, emit }
完整链路:
- 父组件把
onChange写在子组件 vnode 的props上; resolveProps因为它以on开头,把它归入instance.props;- 子组件在
setup中调用emit('change', 1, 2); emit拼出onChange,从instance.props取出处理函数并调用。
emit直接读instance.props,而不是读 setup 的第一个参数(只读代理),所以事件处理函数即使没有在props选项中声明也没问题。 本例只做了首字母大写处理,没有做 camelCase / kebab-case 的归一化,也没有emits选项校验。 事件处理函数本身就是 props,因此它的变化同样走 12.4 的被动更新链路。
四、测试
const MyComponent = {
// ...
setup(props, { emit }) {
// 发射 change 事件,并传递两个参数
emit('change', 1, 2)
return { bar: 'hello setUp' }
}
}
控制台输出
emit change 1 2;如果把onChange去掉,则命中事件不存在分支。
相关
- 12.5 setup 函数的作用与实现:setupContext 的由来与 setup 返回值的处理
- 12.7 插槽的工作原理与实现:继续往 setupContext 里补上 slots