1. 事件定义
在 vnode 中,事件通过 on 前缀的属性定义在 props 中:
const vnode = {
type: 'button',
props: {
onClick: () => console.log('clicked'),
onContextmenu: () => console.log('context menu')
},
children: 'Click Me'
}
javascript
2. 事件绑定
在 patchProps 中识别 on 前缀的属性,使用 addEventListener 绑定:
function patchProps(el, key, prevValue, nextValue) {
if (key.startsWith('on')) {
// 事件处理
const name = key.slice(2).toLowerCase() // onClick → click
el.addEventListener(name, nextValue)
} else if (key === 'class') {
// ...
} else {
// ...
}
}
javascript
3. 事件更新(invoker 模式)
简单方案的问题
直接使用 removeEventListener + addEventListener 更新事件,性能较差:
// ❌ 性能差:每次更新都要 remove + add
if (prevValue) {
el.removeEventListener(name, prevValue)
}
if (nextValue) {
el.addEventListener(name, nextValue)
}
javascript
invoker 代理模式
Vue 使用 invoker(事件调用器)模式,避免频繁操作 DOM 事件绑定:
function patchEvent(el, key, prevValue, nextValue) {
const name = key.slice(2).toLowerCase()
// 获取或创建 invoker 存储
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokers[key]
if (nextValue) {
if (existingInvoker) {
// 更新:只替换 value,不重新绑定事件
existingInvoker.value = nextValue
} else {
// 首次绑定:创建 invoker 函数
const invoker = (e) => {
// 执行时调用 invoker.value(真正的事件处理函数)
if (Array.isArray(invoker.value)) {
invoker.value.forEach(fn => fn(e))
} else {
invoker.value(e)
}
}
invoker.value = nextValue
invokers[key] = invoker
el.addEventListener(name, invoker)
}
} else if (existingInvoker) {
// 移除事件
el.removeEventListener(name, existingInvoker)
delete invokers[key]
}
}
javascript
invoker 工作原理
addEventListener 绑定的不是用户的 handler,而是 invoker 函数
│
▼
invoker 函数内部调用 invoker.value(用户的真正 handler)
│
▼
更新时:只修改 invoker.value 的引用,无需重新绑定/解绑事件
text
支持多个相同事件
当同一事件有多个处理函数时,invoker.value 是一个数组:
const vnode = {
type: 'button',
props: {
onClick: [handler1, handler2] // 数组形式
}
}
// invoker 内部处理
if (Array.isArray(invoker.value)) {
invoker.value.forEach(fn => fn(e))
} else {
invoker.value(e)
}
javascript
多个不同事件
每个事件类型的 invoker 存储在 el._vei 对象的不同键上:
el._vei = {
onClick: invoker1,
onContextmenu: invoker2,
onMousemove: invoker3
}
javascript
性能对比
| 方案 | 绑定/解绑次数 | 适用场景 |
|---|---|---|
| removeEventListener + addEventListener | 每次更新 2 次 | 简单但低效 |
| invoker 模式 | 仅首次绑定 1 次 | Vue 3 采用,高效 |
invoker 模式的核心优势:将 DOM 事件绑定操作从"每次更新"降为"仅首次",后续更新只修改 JavaScript 对象属性,避免了频繁的 DOM API 调用。
Vue 源码中的事件冒泡处理
Vue 3 还通过 timeStamp 比较来处理事件冒泡的时序问题。在 el 上记录事件触发的时间戳,与 invoker 上记录的时间戳比较,确保事件按正确顺序触发。这与 invoker 的代理模式思路一致——通过在 JavaScript 层面控制事件行为。
↑