在模板里写一行 @click="handler"
,背后却是一套从编译器到渲染器、再到 DOM 事件再绑定的精密流水线。本文带你拆解事件在 Vue 内部的完整生命周期。
一、事件的「身份」:props 的一种
模板:
vue
<p @click="clickHandler">text</p>
经过编译器后,事件被翻译成虚拟节点里的一个普通属性:
js
const vnode = {
type: 'p',
props: {
onClick: clickHandler // 注意首字母大写
},
children: 'text'
}
因此,在渲染器眼里,事件与 id
、class
没有本质区别,只是 key 以 on
开头。
二、首次挂载
渲染器在 patchProps
阶段发现 props key 以 on
开头,就进入事件分支:
js
if (/^on/.test(key)) {
const eventName = key.slice(2).toLowerCase() // onClick -> click
el.addEventListener(eventName, nextValue)
}
此时事件处理器被直接绑定到 DOM 节点,首屏即生效。
三、更新策略
如果每次更新都「先 remove 再 add」,会带来无谓的性能损耗。Vue 采用 invoker 代理 解决这一问题:
- 在元素上新建一个隐藏属性
el._vei
(Vue Event Invoker),它是一个对象。 - 对象的键是事件名,值是一个固定引用的代理函数
invoker
。 invoker
内部持有真正的事件数组或函数,更新时只需替换引用,无需重新绑定。
核心逻辑:
js
const invokers = el._vei || (el._vei = {})
let invoker = invokers[eventName]
if (!invoker) {
invoker = invokers[eventName] = (e) => {
const fns = invoker.value
Array.isArray(fns) ? fns.forEach(fn => fn(e)) : fns(e)
}
el.addEventListener(eventName, invoker)
}
invoker.value = nextValue // 指向最新处理器
当 nextValue
是空数组或 null
时,直接 removeEventListener
即可卸载。
四、多处理器与同类型多事件
模板支持数组语法:
vue
<button @click="[fn1, fn2]" @contextmenu="fn3" />
invoker 内部检测到 Array.isArray(value)
,就循环执行所有函数,实现「同类型多处理器」零成本扩展。
总结
Vue 的事件系统用一句话概括:
事件即 props,props 即数据,数据变化即更新,更新即最小化 DOM 操作。
理解 invoker 代理模式,你就拥有了在源码层面定位事件问题、优化事件性能的全部钥匙。