Vue.js设计与实现之十三-渲染器的核心功能:挂载与更新02

开发 前端
在Vue.js的事件处理先要解决的问题,就是如何在虚拟节点中描述事件,事件是一种特殊的属性,在vnode.props对象中以字符串on开头的属性都被视作事件。

1、写在前面

在上篇文章中介绍了虚拟节点的挂载与更新,以及虚拟DOM节点上的属性设置,封装了新的卸载函数unmount。那么,虚拟节点上的事件又是如何处理的呢,同一个事件设置多个处理函数,同一个元素绑定多个事件,触发事件和绑定事件的时机问题应该如何处理?

2、事件的处理

在Vue.js的事件处理先要解决的问题,就是如何在虚拟节点中描述事件,事件是一种特殊的属性,在vnode.props对象中以字符串on开头的属性都被视作事件。

const vnode = {
type:"p",
props:{
// 同一个事件多个事件处理函数
onClick:[
()=>{
//...
},
()=>{
//...
}
],
// 同一个元素绑定多个事件
onContextMenu(){
//...
}
},
children:"text"
}
renderer.render(vnode, document.querySelector("#app"));

在上面代码中,我们看到同一的DOM元素上可以绑定多个事件,同一个事件上又可以有多个事件处理函数。多次我们修改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] => {
//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(key === "class"){
//...
}else if(shouleSetAsProps(el, key, nextValue)){
//...
}else{
//...
}
}
}

在上面代码中,先通过/^on/.test(key)检测元素上以on开头的属性,在绑定事件时伪造事件处理函数invoker

  • 如果invoker不存在时,将invoker作为事件处理函数,缓存到el._vei属性中
  • 将真正的事件处理函数设置为invoker.value属性的值,伪造的事件处理函数invoker绑定到元素上

将el._vei的数据结构设计为一个对象,键即为事件名称,值为对应的事件处理函数,这样就不会出现事件覆盖的现象。当上面invoker.value的类型是数组时,数组中的每个元素都是一个独立的事件处理函数,且这些事件处理函数都能够正确绑定到对应元素上。

3、事件冒泡与更新时机问题

在事件处理中,需要注意处理事件冒泡和更新时机结合导致的问题,事件触发的时间会早于事件处理函数被绑定的时间。

const {effect, ref} = VueReactivity;
const bol = ref(false);
effect(()=>{
//创建vnode
const vnode = {
type:"div",
props:bol.value ? {
onClick(){
//...
}
}:{},
children:[{
type:"p",
props:{
onClick(){
bol.value = true;
}
},
children:"pingping"
}]
}
//渲染vnode
renderer.render(vnode, document.querySelector("#app"));
})

在上面代码中进行理论分析,首次渲染后由于bol.value的初始值为false,对此渲染器并不会给div元素绑定点击事件。在鼠标点击p元素后,bol.value的值变更为true,看到点击事件会从子元素p冒泡到父元素div上,但是div元素又没有绑定事件,因此啥也不发生。

但是,事实上在点击p元素时,父元素div的click事件触发了执行函数的执行。这是因为bol是个响应式数据,在点击p元素后,bol.value的值发生改变,会触发副作用函数的重新执行。而在更新阶段,渲染器会给div元素绑定click事件,在更新完后点击事件才从p元素冒泡到div元素。

触发事件的时机与事件绑定的时机的联系

在一个事件触发时,目标元素上还没有绑定相关的事件处理函数,因此屏蔽所有绑定事件时机要晚于触发时间的事件处理函数的执行。

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.timeStamp是事件发生的时间,如果事件触发的时机早于事件绑定的时间,则不执行事件处理函数
if(e.timeStamp < invoker.attached) return;
//invoker.value是数组时,遍历逐个调用事件处理函数
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(key === "class"){
//...
}else if(shouleSetAsProps(el, key, nextValue)){
//...
}else{
//...
}
}
}

在上面代码中,给伪造的事件处理函数添加了invoker.attached属性,用于存储事件处理函数被绑定的时间。在invoker执行的时候,通过事件对象e.timeStamp获取事件发生的时间,比较两者的时间,如果事件触发的时机早于事件绑定的时间,则不执行事件处理函数。

4、写在最后

在本文中主要讨论了事件的处理,介绍了在虚拟节点上绑定事件,如何绑定和更新事件。同时,还介绍了如何处理触发事件与更新时机的问题,屏蔽所有绑定事件时机要晚于触发时间的事件处理函数的执行。

责任编辑:姜华 来源: 前端一码平川
相关推荐

2022-04-19 23:01:54

Vue.jsDOM节点DOM树

2022-04-18 08:09:44

渲染器DOM挂载Vue.js

2022-04-01 08:08:27

Vue.js框架命令式

2022-04-25 07:36:21

组件数据函数

2022-04-12 08:08:57

watch函数options封装

2022-04-04 16:53:56

Vue.js设计框架

2022-04-03 15:44:55

Vue.js框架设计设计与实现

2022-04-11 08:03:30

Vue.jscomputed计算属性

2022-05-03 21:18:38

Vue.js组件KeepAlive

2022-04-14 09:35:03

Vue.js设计Reflect

2022-04-05 16:44:59

系统Vue.js响应式

2022-04-26 05:55:06

Vue.js异步组件

2022-04-17 09:18:11

响应式数据Vue.js

2022-04-09 17:53:56

Vue.js分支切换嵌套的effect

2022-04-16 13:59:34

Vue.jsJavascript

2022-04-12 08:09:22

Nodejs前端面试题

2019-04-01 19:38:28

Vue.jsJavascript前端

2010-08-13 11:02:27

Flex渲染器

2009-07-15 13:48:26

Swing模型和渲染器

2009-07-16 10:26:49

渲染器接口Swing
点赞
收藏

51CTO技术栈公众号