如何在虚拟节点中描述事件
事件可以视为一种特殊的属性,因此可以约定,在vnode.props对象中,凡是以字符串on开头的属性都视作事件。例如:
const vnode = {
type: 'p',
props: {
// 使用 onXxx描述事件
onClick: () => {
alert('clicked')
}
},
children: 'text'
}
要将事件添加到DOM元素上,只需要在patchProps中调用addEventListener函数来绑定事件即可,如下面代码所示:
patchProps(el, key, prevValue, nextValue){
// 匹配以on开头的属性,试其为事件
// 正则表达式
if(/^on/.test(key)){
// 根据属性名称得到对应的事件名称,例如 onClick ---> click
const name = key.slice(2).toLowerCase()
// 绑定事件,nextValue为事件处理函数
el.addEventListener(name,nextValue)
}else if(key === 'class'){
// 省略部分代码
}else if(shouldSetAsProps(el,key,nextValue)){
// 省略部分代码
}else{
// 省略部分代码
}
}
那么更新事件要怎么处理?按照一般的思路,需要先移除之前添加的事件处理函数,然后再将新的事件处理函数绑定到DOM元素上,如下面代码所示:
patchProps(el, key, prevValue, nextValue){
if(/^on/.test(key)){
const name = key.slice(2).toLowerCase()
// 移除上一次绑定的事件处理函数
preValue && el.removeEventListener(name, prevValue)
// 绑定新的事件处理函数
el.addEventListener(name, nextValue)
}else if(key === 'class'){
// 省略部分代码
}else if(shouldSetAsProps(el,key,nextValue)){
// 省略部分代码
}else{
// 省略部分代码
}
}
这样做代码能够按预期工作,但是有一种性能更好的方式来完成事件更新。在绑定事件时,可以绑定一个伪造的事件处理函数invoker,然后把真正的事件处理函数设置为Invoker.value属性的值。这样当更新事件的时候,不需要调用removeEventListener函数,只需要更新invoker.value的值即可,如下面代码所示:
patchProps(el, key, prevValue, nextValue){
if(/^on/.test(key)){
// 获取为该元素伪造的事件处理函数invoker
let invoker = el.vei
const name = key.slice(2).toLowerCase()
if(nextValue){
if(!invoker){
// 如果没有invoker,则将一个伪造的invoker缓存到el._vei中
// vei是vue event invoker的首字母缩写
invoker = el.vei = (e)=>{
// 当伪造的事件处理函数执行时,会执行真正的事件处理函数
invoker.value(e)
}
// 将真正的事件处理函数复制给invoker.value
invoker.value = nextValue
// 绑定invoker作为事件处理函数
el.addEventListener(name,invoker)
}else{
// 如果invoker存在,意味着更新,并且只需要更新invoker.value即可
invoker.value = nextValue
}
}else if(invoker){
// 新的事件绑定函数不存在,且之前绑定的Invoker存在,则移除绑定
el.removeEventListener(name, invoker)
}
}else if(key === 'class'){
// 省略部分代码
}else if(shouldSetAsProps(el,key,nextValue)){
// 省略部分代码
}else{
// 省略部分代码
}
}
但目前的实现仍然存在问题,现在将事件处理函数缓存在el._vei属性中,但这样的话同一时刻只能缓存一个事件处理函数。也就是说,如果一个元素同时绑定了多种事件,将会出现事件覆盖的现象。为了解决这个问题,应该将el._vei设计为一个对象,其键是事件名称,值是对应的事件处理函数,这样就不会发生事件覆盖的现象了。如下代码所示:
patchProps(el, key, prevValue, nextValue){
if(/^on/.test(key)){
//定义el._vei为一个对象,存在事件名称到事件处理函数的映射
const invokers = el._vei || (el._vei = {})
// 根据事件名称获取Invoker
let invoker = invokers[key]
const name = key.slice(2).toLowerCase()
if(nextValue){
if(!invoker){
// 将事件处理函数缓存到el._vei[key]下,避免覆盖
invoker = el.vei[key] = (e)=>{
invoker.value(e)
}
invoker.value = nextValue
el.addEventListener(name,invoker)
}else{
invoker.value = nextValue
}
}else if(invoker){
el.removeEventListener(name, invoker)
}
}else if(key === 'class'){
// 省略部分代码
}else if(shouldSetAsProps(el,key,nextValue)){
// 省略部分代码
}else{
// 省略部分代码
}
}
对于同一类型的事件而言,还可以绑定多个事件处理函数。在原生DOM编程中,多次调用addEventListener函数为元素绑定同一类型的事件时,多个事件处理函数可以共存,例如:
el.addEventListener('click', fn1)
el.addEventListener('click', fn2)
点击元素时,事件处理函数fn1和fn2都会执行。因此,为了描述同一个事件的多个事件处理函数,需要调整vnode.props对象中事件的数据结构,如下面代码所示:
const vnode = {
type: 'p',
props: {
onClick: [
// 第一个事件处理函数
() => {
console.log('clicked 1')
},
// 第二个事件处理函数
() => {
console.log('clicked 2')
},
]
},
children: 'text'
}
renderer.render(vnode, document.querySelector('#app'))
使用数组来描述事件,数组中每个元素都是一个独立的事件处理函数,并且这些事件处理函数都能正确地绑定到对应元素上。这时需要修改patchProps函数中事件处理相关的代码,如下面代码:
patchProps(el, key, prevValue, nextValue){
if(/^on/.test(key)){
const invokers = el._vei || (el._vei = {})
let invoker = invokers[key]
const name = key.slice(2).toLowerCase()
if(nextValue){
if(!invoker){
invoker = el.vei[key] = (e)=>{
// 如果invoker.value是数组,则遍历它并逐个调用事件处理函数
if(Array.isArray(invoker.value)){
invoker.value.forEach(fn=>fn(e))
}else{
// 否则直接作为函数调用
invoker.value(e)
}
}
invoker.value = nextValue
el.addEventListener(name,invoker)
}else{
invoker.value = nextValue
}
}else if(invoker){
el.removeEventListener(name, invoker)
}
}else if(key === 'class'){
// 省略部分代码
}else if(shouldSetAsProps(el,key,nextValue)){
// 省略部分代码
}else{
// 省略部分代码
}
}
当invoker函数执行时,在调用真正的事件处理函数之前,先检查invoker.value的数据结构是否是数组,如果是数组则遍历它,并逐个调用定义在数组中的事件处理函数