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


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

    事件可以视为一种特殊的属性,因此可以约定,在vnode.props对象中,凡是以字符串on开头的属性都视作事件。例如:

    const vnode = {
    	type: 'p',
    	props: {
    		// 使用 onXxx描述事件
    		onClick: () => {
    			alert('clicked')
    		}
    	},
    	children: 'text'
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    要将事件添加到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{
    		// 省略部分代码
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    那么更新事件要怎么处理?按照一般的思路,需要先移除之前添加的事件处理函数,然后再将新的事件处理函数绑定到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{
    		// 省略部分代码
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这样做代码能够按预期工作,但是有一种性能更好的方式来完成事件更新。在绑定事件时,可以绑定一个伪造的事件处理函数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{
    		// 省略部分代码
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    但目前的实现仍然存在问题,现在将事件处理函数缓存在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{
    		// 省略部分代码
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    对于同一类型的事件而言,还可以绑定多个事件处理函数。在原生DOM编程中,多次调用addEventListener函数为元素绑定同一类型的事件时,多个事件处理函数可以共存,例如:

    el.addEventListener('click', fn1)
    el.addEventListener('click', fn2)
    
    • 1
    • 2

    点击元素时,事件处理函数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'))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    使用数组来描述事件,数组中每个元素都是一个独立的事件处理函数,并且这些事件处理函数都能正确地绑定到对应元素上。这时需要修改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{
    		// 省略部分代码
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    当invoker函数执行时,在调用真正的事件处理函数之前,先检查invoker.value的数据结构是否是数组,如果是数组则遍历它,并逐个调用定义在数组中的事件处理函数

  • 相关阅读:
    预测宝可梦武力值、分类宝可梦
    Splunk Enterprise 9.0.0 (macOS, Linux, Windows) -- 机器数据管理和分析
    经典模型——NiN&GoogLeNet
    STL系列文章
    武汉新时标文化传媒有限公司抖音小店如何提高出单量?
    NumPy 通用函数(ufunc):高性能数组运算的利器
    前端面试问题(jwt/布局/vue数组下标/扁平化/菜单树形/url api/新版本)
    RIP协议
    java8 stream list 操作
    资源哟资源网正版模板v1.3
  • 原文地址:https://blog.csdn.net/loyd3/article/details/125841864