phenomLi / Blog

Comments, Thoughts, Conclusions, Ideas, and the progress.
219 stars 17 forks source link

随便聊聊事件委托 #11

Open phenomLi opened 6 years ago

phenomLi commented 6 years ago

在通常情况下,我们给一组DOM节点添加相同的事件,都是使用循环绑定:

Array.from(document.querySelectorAll('li'))
    .map(li => li.addEventListener('click', handler));

当节点数量比较少的时候,这是最简单直接的方法,没有问题。但是当节点数量达到一定数量级的时候(数千甚至上万,比如表格应用),给这么多个节点都循环绑定相同一个事件,显然会造成资源浪费,而且影响性能。

为什么浪费资源和影响性能

很多人在谈到大量节点的事件绑定的时候,都说浪费资源,但是很少会提到说为什么会浪费资源,下面我就来分析一下。
首先,在初始化页面的时候,我们先要对这些节点进行一次遍历,DOM节点(也就是HTMLElement)是一个很大很庞杂的对象,遍历DOM节点要比遍历普通数组更耗时。
我们来看看一个DOM节点(HTMLElement)究竟包含了什么东西:
很多很恐怖,这只是一个简单的只有文本子节点的li,这也说明了为什么使用虚拟DOM会更快。 第二,在我们遍历节点的同时,我们还要给每个节点添加事件,也就是给节点(或者其proto)添加二极的DOM事件。 另外,大家都知道,为每个DOM节点绑定事件后都会有一个event对象被当做参数传入事件函数里面,event对象包含了当前事件发生的信息,这个event对象也是一个很庞大的对象:
所以,总结起来,大量DOM节点的事件绑定造成资源浪费和性能损失原因主要有3:



使用事件委托

什么是委托?顾名思义,其实就是一件事自己不做,叫别人做。所以事件委托就是将节点自己本身要绑定的事件绑到别人身上,那么绑到哪里呢,最好就是绑定到父节点(其实不是父节点也行,document也可以)。 也就是说,事件委托的基本原理就是,如果我们要给某个节点A绑定事件,那么我们可以给这个节点A的祖先节点绑定事件,然后在祖先节点触发事件的时候,判断鼠标指向的节点是否为节点A,若是就响应事件,若不是就什么也不干。 显然,使用事件委托,我们只需要为一个节点绑定事件,而不需要简单粗暴地为每个多个节点绑定相同的事件,大大节省了内存资源,优化了性能。
那么具体究竟怎么实现事件委托呢? 我上面提到过,在事件发生的时候,会产生一个event对象,用作保存事件发生的信息,这个event就是实现事件绑定的关键。在event对象里面有一个target属性,我们可以通过访问这个target属性,知道鼠标当前响应的是哪一个节点。然后再判断这个节点是否是应该响应的节点。 顺着这个思路,我们就很容易把一个简单的事件委托实现出来:

<ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
        <li>6</li>
        <li>7</li>
</ul>
const handler = function() {
    console.log(this);
}

document.querySelector('ul').addEventListener('click', function(e) {
    //访问event.target,看当前响应事件的节点是否为li
    if(e.target.tagName.toLowerCase() === 'li') {
        //若是li,则执行handler,同时讲handler的上下文修正为li该li节点
        handler.call(e.target);
    }
});

以上就是委托事件的一个简单实现。

顺便说说React中事件委托的实现

为了让性能发挥到极致,在React中,把所有事件全部委托到了document元素,也就是说所有元素的事件都基于document。那么问题就来了,如果像上面实现那样,只判断tagName,肯定是不现实的,因为一个html里面相同标签的太多了。React的解决办法是给每一个从虚拟dom映射出来的DOM元素都添加一个唯一的id,类似于这样: 然后有了这个id,就可以进行节点判断了:

document.addEventListener('click', function(e) {

    //访问event.target,判断id
    if(e.target.getAttribute('element-id').subString(0, 4) === '0-0-1') {

        //执行handler
        handler.call(e.target);
    }
});

当然,React中使用这个节点id的当然不只只是因为事件委托这么简单,id还有一个很重要的功能就是对diff算法进行优化,这些都是题外话了。