Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

随便聊聊事件委托 #11

Open
phenomLi opened this issue Nov 18, 2017 · 0 comments
Open

随便聊聊事件委托 #11

phenomLi opened this issue Nov 18, 2017 · 0 comments

Comments

@phenomLi
Copy link
Owner

phenomLi commented Nov 18, 2017

在通常情况下,我们给一组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:

  • DOM节点循环的耗时
  • 给每个DOM节点添加事件
  • 每个event对象的创建


使用事件委托

什么是委托?顾名思义,其实就是一件事自己不做,叫别人做。所以事件委托就是将节点自己本身要绑定的事件绑到别人身上,那么绑到哪里呢,最好就是绑定到父节点(其实不是父节点也行,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算法进行优化,这些都是题外话了。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant