【vue设计与实现】挂载和更新 6-事件冒泡与更新时机问题

首先来看一个例子:

const {effect, ref} = VueReactivity

const bol = ref(false)

effect(()=>{
	// 创建node
	const vnode = {
		type: 'div',
		props: bol.value?{
			onClick: ()=>{
				alert('父元素 clicked')
			}
		}:{}
		children: [
			{
				type: 'p',
				props: {
					onClick: ()=>{
						bol.value = true
					}
				},
				children: 'text'
			}
		]
	}
	//渲染vnode
	renderer.render(vnode,document.querySelector('#app'))

})

在这个例子中,vnode对象描述了一个div元素,并且该div元素具有一个p元素作为子节点。
其中,div元素的props对象的值是由一个三元表达式决定。首次渲染时,由于bol.value的值为false,所以它的props值是一个空对象。
而p元素具有click点击事件,并且当点击它时,事件处理函数会将bol.value的值设置为true.

那么当首次渲染完成后,用鼠标点击p元素,会触发父级div元素的click事件处理函数执行吗?
理论上来说,在首次渲染完成之后,由于bol.value的值为false,所以渲染器不会为div元素绑定点击事件。点击p冒泡到父元素div,因为div没有绑定click时间,所以什么都应该不发省。
但是事实是父级div元素的click事件的事件处理函数竟然执行了。这其实跟更新机制有关。首先来分析下点击p元素时,到底发生了什么。
点击p元素时,其身上的click函数会执行,bol.value的值改为true。由于bol是一个响应式数据,所以当值发生变化的时候,会触发副作用函数重新执行,在更新截断,渲染器会为父级div元素绑定click事件处理函数。当更新完成后,点击事件才从p元素冒泡到父级div元素。而这时div元素上已经绑定了click事件的处理函数。
就是说,是因为更新操作发生在事件冒泡之前,即为div元素绑定事件处理函数发生在事件冒泡之前

那么要怎么解决这个问题?
可以发现,事件触发的时间要早于事件处理函数被绑定的时间。也就是说,当一个事件触发时,目标元素上还没有绑定相关的事件处理函数,那么可以这样:屏蔽所有绑定事件晚于时间触发时间的事件处理函数的执行。我们可以调整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) => {
					// e.timeStamp 是事件发生的时间
					// 如果事件发生的时间早于事件处理函数绑定的时间,则不执行时间处理函数
					if(e.timeStamp < invoker.attacher) return
					if(Array.isArray(invoker.value)){
						invoker.value.forEach(fn=>fn(e))
					}else{
						invoker.value(e)
					}
				}
				invoker.value = nextValue
				// 添加 invoker.attached属性,存储事件处理函数被绑定的时间
				invoker.attached = performance.now()
				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.attached属性,用来存储时间处理函数被绑定的时间。然后,在invoker执行的时候,通过时间对象的e.timeStamp获取事件发生的事件。最后,比较两者,如果事件处理函数被绑定的时间晚于事件发生的时间,则不执行该事件处理函数。

这里,在关于事件方面,performance.now获取的是高精时间,但根据浏览器的不同,e.timeStamp的值会有所不同,既可以是高精时间也可能是非高精时间。因此要做兼容处理,但是在Chrome 49, Firefox 54, Opera 36之后的版本,e.timeStamp都是高精时间。

你可能感兴趣的:(vue设计与实现,笔记,vue.js,javascript,前端,前端框架,面试)