【vue设计与实现】挂载和更新 5-事件的处理

如何在虚拟节点中描述事件

事件可以视为一种特殊的属性,因此可以约定,在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的数据结构是否是数组,如果是数组则遍历它,并逐个调用定义在数组中的事件处理函数

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值