引言
本节我们将讨论如何处理事件,包括如何在虚拟节点中描述事件,如何把事件添加到 DOM 元素上,以及如何更新事件。我们先来解决第一个问题,即如何在虚拟节点中描述事件。事件可以视作一种特殊的属性,因此我们可以约定,在 vnode.props 对象中,凡是以字符串 on 开头的属性都视作事件。例如:
const vnode = {
type: 'p',
props: {
// 使用 onXxx 描述事件
onClick: () => {
alert('clicked');
}
},
children: 'text'
};
解决了事件在虚拟节点层面的描述问题后,我们再来看看如何将事件添加到 DOM 元素上。这非常简单,只需要在 patchProps 中调用 addEventListener 函数来绑定事件即可,如下面的代码所示:
function patchProps(el, key, prevValue, nextValue) {
// 匹配以 on 开头的属性,视其为事件
if (/^on/.test(key)) {
// 根据属性名称得到对应的事件名称,例如 onClick ---> click
const name = key.slice(2).toLowerCase();
// 移除上一次绑定的事件处理函数
prevValue && el.removeEventListener(name, prevValue);
// 绑定新的事件处理函数
el.addEventListener(name, nextValue);
} else if (key === 'class') {
// 省略部分代码(处理 class 属性的逻辑)
} else if (shouldSetAsProps(el, key, nextValue)) {
// 省略部分代码(处理其他属性的逻辑)
} else {
// 省略部分代码(处理其他属性的逻辑)
}
}
事实上可以更为优化的事件更新机制,避免多次调用 removeEventListener
和 addEventListener
。
function patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
const name = key.slice(2).toLowerCase();
let invoker = el.__vei || (el.__vei = {});
if (nextValue) {
if (!invoker[name]) {
// 如果没有 invoker,则创建一个伪造的 invoker 函数
invoker[name] = (e) => {
invoker[name].value(e);
};
}
// 将真正的事件处理函数赋值给 invoker 函数的 value 属性
invoker[name].value = nextValue;
// 绑定 invoker 函数作为事件处理函数
el.addEventListener(name, invoker[name]);
} else if (invoker[name]) {
// 如果新的事件处理函数不存在,且之前绑定的 invoker 存在,则移除绑定
el.removeEventListener(name, invoker[name]);
invoker[name] = null;
}
} else if (key === 'class') {
// 省略部分代码(处理 class 属性的逻辑)
} else if (shouldSetAsProps(el, key, nextValue)) {
// 省略部分代码(处理其他属性的逻辑)
} else {
// 省略部分代码(处理其他属性的逻辑)
}
}
观察上面的代码,事件绑定主要分为两个步骤。先从 el._vei 中读取对应的 invoker,如果 invoker 不存在,则将伪造的 invoker 作为事件处理函数,并将它缓存到el._vei 属性中。
把真正的事件处理函数赋值给 invoker.value 属性,然后把伪造的 invoker 函数作为事件处理函数绑定到元素上。可以看到,当事件触发时,实际上执行的是伪造的事件处理函数,在其内部间接执行了真正的事件处理函数 invoker.value(e)。
当更新事件时,由于 el._vei 已经存在了,所以我们只需要将invoker.value 的值修改为新的事件处理函数即可。
这样,在更新事件时可以避免一次 removeEventListener 函数的调用,从而提升了性能。实际上,伪造的事件处理函数的作用不止于此,它还能解决事件冒泡与事件更新之间相互影响的问题。但目前的实现仍然存在问题。现在我们将事件处理函数缓存在el._vei 属性中,问题是,在同一时刻只能缓存一个事件处理函数。这意味着,如果一个元素同时绑定了多种事件,将会出现事件覆盖的现象。
const vnode = {
type: 'p',
props: {
// 使用 onXxx 描述事件
onClick: () => {
alert('clicked');
},
onContextmenu: () => {
alert('contextmenu');
}
},
children: 'text'
};
// 假设 renderer 是你的渲染器对象
renderer.render(vnode, document.querySelector('#app'));
当渲染器尝试渲染这上面代码中给出的 vnode 时,会先绑定click 事件,然后再绑定 contextmenu 事件。后绑定的contextmenu 事件的处理函数将覆盖先绑定的 click 事件的处理函
数。为了解决事件覆盖的问题,我们需要重新设计 el._vei 的数据结构。我们应该将 el._vei 设计为一个对象,它的键是事件名称,它的值则是对应的事件处理函数,这样就不会发生事件覆盖的现象了.
根据你提供的代码片段,这段代码主要是用于处理 DOM 元素的属性更新,其中包括事件的绑定和解绑逻辑。在这个代码中,它使用了一个 el._vei
的对象来缓存事件处理函数。下面是你提供的代码的一些修正:
function patchProps(el, key, prevValue, nextValue) {
if (/^on/.test(key)) {
const invokers = el._vei || (el._vei = {});
const name = key.slice(2).toLowerCase();
let invoker = invokers[name];
if (nextValue) {
if (!invoker) {
invoker = el._vei[name] = (e) => {
if (Array.isArray(invoker.value)) {
invoker.value.forEach(fn => fn(e));
} else {
invoker.value(e);
}
};
}
invoker.value = nextValue;
el.addEventListener(name, invoker);
} else if (invoker) {
el.removeEventListener(name, invoker);
el._vei[name] = null;
}
} else if (key === 'class') {
// 处理 class 属性的逻辑
} else if (shouldSetAsProps(el, key, nextValue)) {
// 处理其他属性的逻辑
} else {
// 处理其他属性的逻辑
}
}
在这段代码中,我们修改了 invoker 函数的实现。当 invoker函数执行时,在调用真正的事件处理函数之前,要先检查invoker.value 的数据结构是否是数组,如果是数组则遍历它,并逐个调用定义在数组中的事件处理函数。
評論