阅读 743

你不知道的冒泡

开始

冒泡大家肯定都非常熟悉,但是上周在看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,父元素没有绑定事件,子元素绑定了点击事件,并同时给父元素绑定了一个点击事件,可以看一看,点击子元素之后输出的结果是怎么样的。

image.png

是的,新绑定的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;
}
复制代码

原理很简单,事件触发时,有一个 timeStampcreateInvoker记录了元素绑定的时间,在接受到事件的时候,通过 timeStampattached 的比较,来决定是否运行这个事件绑定的 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);
  })
})
复制代码

这时,再到浏览器运行一下,就能发现能得到我们想要的结果了。

image.png

点击第一次只有 child 打印,第二次点击,childfather 一起打印。

结语

自己实现的代码,和vue中的还是有区别的,vue中,如果之前dom中就绑定了事件,那么事件的更改,是不会重新 createInvoker 的,只有在原来没有绑定事件,在进行 patch 时,新绑定的事件,才会进行这样的处理。

如果有写的不对的,欢迎大家纠正,接下来的一大段时间,可能都会去研究下 vue3 的源码,遇到不懂的代码,或者感兴趣的,都会整理成小文章发出来。

最终目标肯定是全部读完之后整篇大的,不知道这个过程会有多久,但无论快或者慢,能坚持下去就算成功了,与君共勉~

文章分类
前端
文章标签