8.7 事件的处理
事件可以视作一种特殊属性:
vnode.props中以on开头的键即事件;难点不在绑定,而在更新。
一、在虚拟节点中描述事件
约定:vnode.props 中以字符串 on 开头的属性都视作事件。
const vnode = {
type: 'button',
props: {
// 使用 onXxx 描述事件
onClick: () => {
alert('clicked')
}
},
children: 'click me !'
}
patchProps 中据此分派,并把属性名还原成事件名(onClick → click):
if (/^on/.test(key)) {
// 匹配以 on 开头的属性,视其为事件
const name = key.slice(2).toLowerCase()
}
二、最直白的实现及其缺陷
在 patchProps 中直接调 addEventListener 绑定事件;更新事件时,需要先移除之前添加的处理函数,再绑定新的:
prevValue && el.removeEventListener(name, prevValue)
el.addEventListener(name, nextValue)
问题:每次更新都要走一遍 removeEventListener + addEventListener,即使变化的只是处理函数的内容。
三、invoker:伪造的事件处理函数
更优的做法是绑定一个伪造的处理函数 invoker,把真正的事件处理函数放在 invoker.value 上:
if (/^on/.test(key)) {
// 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
const invokers = el._vei || (el._vei = {})
let invoker = invokers[key]
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
// 没有 invoker → 创建并缓存到 el._vei[key](vei = vue event invoker 的首字母缩写)
invoker = el._vei[key] = (e) => {
// 伪造的处理函数执行时,执行真正的事件处理函数
invoker.value(e)
}
// 将真正的事件处理函数赋值给 invoker.value
invoker.value = nextValue
el.addEventListener(name, invoker)
} else {
// invoker 已存在 → 更新时只需更新 invoker.value
invoker.value = nextValue
}
} else if (invoker) {
// 新的绑定函数不存在,但之前绑过 → 移除绑定
el.removeEventListener(name, invoker)
}
}
好处:
- 更新事件时不再调用
removeEventListener,省下一次 DOM API 调用,提升性能; - 更重要的是,它还能解决事件冒泡与事件更新相互影响的问题(见 8.8)。
四、修 bug:el._vei 必须是对象
若把 el._vei 直接缓存成一个函数,一个元素同时绑定了多种事件时会出现事件覆盖:后绑定的 contextmenu 处理函数会覆盖先绑定的 click 处理函数。
解决:
el._vei设计为对象,键是事件名称,值是对应的 invoker。这也是上面代码用invokers[key]取 invoker 的原因。
五、同一类型事件绑定多个处理函数
原生 DOM 编程中,多次调用 addEventListener 为元素绑定同一类型事件时,多个处理函数可以共存。所以 vnode.props 里的事件值也允许写成数组:
const vnode = {
type: 'button',
props: {
onClick: [fn1, fn2]
}
}
invoker 执行时先检查 invoker.value 的数据结构:
invoker = el._vei[key] = (e) => {
// 见 8.8:绑定时间晚于事件触发时间的处理函数不执行
if (e.timeStamp < invoker.attached) return
if (Array.isArray(invoker.value)) {
// 数组 → 遍历并逐个调用
invoker.value.forEach(fn => fn(e))
} else {
// 否则直接作为函数调用
invoker.value(e)
}
}
相关
- 8.6 区分vnode类型:
patch如何分派到patchElement - 8.8 事件冒泡与更新时机问题:invoker 的第二个用途