Open phenomLi opened 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:
li
event
什么是委托?顾名思义,其实就是一件事自己不做,叫别人做。所以事件委托就是将节点自己本身要绑定的事件绑到别人身上,那么绑到哪里呢,最好就是绑定到父节点(其实不是父节点也行,document也可以)。 也就是说,事件委托的基本原理就是,如果我们要给某个节点A绑定事件,那么我们可以给这个节点A的祖先节点绑定事件,然后在祖先节点触发事件的时候,判断鼠标指向的节点是否为节点A,若是就响应事件,若不是就什么也不干。 显然,使用事件委托,我们只需要为一个节点绑定事件,而不需要简单粗暴地为每个多个节点绑定相同的事件,大大节省了内存资源,优化了性能。 那么具体究竟怎么实现事件委托呢? 我上面提到过,在事件发生的时候,会产生一个event对象,用作保存事件发生的信息,这个event就是实现事件绑定的关键。在event对象里面有一个target属性,我们可以通过访问这个target属性,知道鼠标当前响应的是哪一个节点。然后再判断这个节点是否是应该响应的节点。 顺着这个思路,我们就很容易把一个简单的事件委托实现出来:
document
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中,把所有事件全部委托到了document元素,也就是说所有元素的事件都基于document。那么问题就来了,如果像上面实现那样,只判断tagName,肯定是不现实的,因为一个html里面相同标签的太多了。React的解决办法是给每一个从虚拟dom映射出来的DOM元素都添加一个唯一的id,类似于这样: 然后有了这个id,就可以进行节点判断了:
tagName
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算法进行优化,这些都是题外话了。
在通常情况下,我们给一组DOM节点添加相同的事件,都是使用循环绑定:
当节点数量比较少的时候,这是最简单直接的方法,没有问题。但是当节点数量达到一定数量级的时候(数千甚至上万,比如表格应用),给这么多个节点都循环绑定相同一个事件,显然会造成资源浪费,而且影响性能。
为什么浪费资源和影响性能
很多人在谈到大量节点的事件绑定的时候,都说浪费资源,但是很少会提到说为什么会浪费资源,下面我就来分析一下。
首先,在初始化页面的时候,我们先要对这些节点进行一次遍历,DOM节点(也就是HTMLElement)是一个很大很庞杂的对象,遍历DOM节点要比遍历普通数组更耗时。
我们来看看一个DOM节点(HTMLElement)究竟包含了什么东西:
很多很恐怖,这只是一个简单的只有文本子节点的
li
,这也说明了为什么使用虚拟DOM会更快。 第二,在我们遍历节点的同时,我们还要给每个节点添加事件,也就是给节点(或者其proto)添加二极的DOM事件。 另外,大家都知道,为每个DOM节点绑定事件后都会有一个event
对象被当做参数传入事件函数里面,event
对象包含了当前事件发生的信息,这个event
对象也是一个很庞大的对象:所以,总结起来,大量DOM节点的事件绑定造成资源浪费和性能损失原因主要有3:
event
对象的创建使用事件委托
什么是委托?顾名思义,其实就是一件事自己不做,叫别人做。所以事件委托就是将节点自己本身要绑定的事件绑到别人身上,那么绑到哪里呢,最好就是绑定到父节点(其实不是父节点也行,
document
也可以)。 也就是说,事件委托的基本原理就是,如果我们要给某个节点A绑定事件,那么我们可以给这个节点A的祖先节点绑定事件,然后在祖先节点触发事件的时候,判断鼠标指向的节点是否为节点A,若是就响应事件,若不是就什么也不干。 显然,使用事件委托,我们只需要为一个节点绑定事件,而不需要简单粗暴地为每个多个节点绑定相同的事件,大大节省了内存资源,优化了性能。那么具体究竟怎么实现事件委托呢? 我上面提到过,在事件发生的时候,会产生一个
event
对象,用作保存事件发生的信息,这个event
就是实现事件绑定的关键。在event
对象里面有一个target
属性,我们可以通过访问这个target
属性,知道鼠标当前响应的是哪一个节点。然后再判断这个节点是否是应该响应的节点。 顺着这个思路,我们就很容易把一个简单的事件委托实现出来:以上就是委托事件的一个简单实现。
顺便说说React中事件委托的实现
为了让性能发挥到极致,在React中,把所有事件全部委托到了
然后有了这个id,就可以进行节点判断了:
document
元素,也就是说所有元素的事件都基于document
。那么问题就来了,如果像上面实现那样,只判断tagName
,肯定是不现实的,因为一个html里面相同标签的太多了。React的解决办法是给每一个从虚拟dom映射出来的DOM元素都添加一个唯一的id,类似于这样:当然,React中使用这个节点id的当然不只只是因为事件委托这么简单,id还有一个很重要的功能就是对diff算法进行优化,这些都是题外话了。