开始
冒泡大家肯定都非常熟悉,但是上周在看vue的源码时候,发现一种很有意思的冒泡场景,直接看代码。
<div id="container">
<div id="div"></div>
</div>
<script>
const div = document.getElementById('div');
const con = document.getElementById('container');
div.addEventListener('click', function (e) {
console.log('child click' + 111111);
con.addEventListener('click', function () {
console.log('father click' + 222222);
})
})
</script>
dom中有父子div,父元素没有绑定事件,子元素绑定了点击事件,并同时给父元素绑定了一个点击事件,可以看一看,点击子元素之后输出的结果是怎么样的。
是的,新绑定的father click也被触发了,这里,我们可以得出,事件冒泡的传递事件,是在绑定的handler触发之后的,但是最大多数情况下,我们肯定是不想让父元素新绑定的 click 触发的,不然图啥呢,直接初始化绑定就好了,也不用写在子元素的 handler 中。
那怎么解决呢,如果加个setTimeout(0)去给父元素添加事件,确实可以解决了这个问题。但是明显让代码的可读性降了一个档次。
解决
vue是异步更新dom的,肯定也会碰到这个问题,我们可以看看vue中是怎么解决这个问题的。
function createInvoker(initialValue, instance) {
const invoker = (e) => {
// async edge case #6566: inner click event triggers patch, event handler
// attached to outer element during patch, and triggered again. This
// happens because browsers fire microtask ticks between event propagation.
// the solution is simple: we save the timestamp when a handler is attached,
// and the handler would only fire if the event passed to it was fired
// AFTER it was attached.
const timeStamp = e.timeStamp || _getNow();
if (skipTimestampCheck || timeStamp >= invoker.attached - 1) {
runtimeCore.callWithAsyncErrorHandling(patchStopImmediatePropagation(e, invoker.value), instance, 5 /* NATIVE_EVENT_HANDLER */, [e]);
}
};
invoker.value = initialValue;
invoker.attached = getNow();
return invoker;
}
原理很简单,事件触发时,有一个 timeStamp, createInvoker记录了元素绑定的时间,在接受到事件的时候,通过 timeStamp 和 attached 的比较,来决定是否运行这个事件绑定的 handler。
用自己的代码重新写一个 createEventListener 来解决这个问题,
Object.prototype.createEventListener = function (...args) {
// handler
const func = args[1];
const invoker = function (...arguments) {
const event = arguments[0];
// 比较event触发的时间,和事件绑定的时间,如果event触发时候,事件还未绑定,则不运行
if (event.timeStamp >= invoker.attached) {
func(...arguments)
}
}
// handler被绑定上的时间
invoker.attached = performance.now();
// 替换原来的handler
args[1] = invoker;
return this.addEventListener(...args);
}
div.createEventListener('click', function (e) {
console.log('child click' + 111111);
con.createEventListener('click', function () {
console.log('father click' + 222222);
})
})
这时,再到浏览器运行一下,就能发现能得到我们想要的结果了。
点击第一次只有 child 打印,第二次点击,child 和 father 一起打印。
结语
自己实现的代码,和vue中的还是有区别的,vue中,如果之前dom中就绑定了事件,那么事件的更改,是不会重新 createInvoker 的,只有在原来没有绑定事件,在进行 patch 时,新绑定的事件,才会进行这样的处理。
如果有写的不对的,欢迎大家纠正,接下来的一大段时间,可能都会去研究下 vue3 的源码,遇到不懂的代码,或者感兴趣的,都会整理成小文章发出来。
最终目标肯定是全部读完之后整篇大的,不知道这个过程会有多久,但无论快或者慢,能坚持下去就算成功了,与君共勉~