JS 事件模型
捕获/冒泡
这是一个事件触发时序的问题,多数情况下这两种区别我们并不需要关心,但如果遇到特殊情况了,那就得能搞清楚原理了。
捕获
有以下两个节点
<div>
<input type="button" value="test" />
</div>
如果在按钮上点击,div就会先捕获到事件,而input会后于div捕获到该事件,以下代码会先输出div
,后输出input
。
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('capture-div');
}, true);
input.addEventListener('click', (e) => {
alert('capture-input');
}, true);
冒泡
有以下两个节点
<div>
<input type="button" value="test" />
</div>
如果在按钮上点击,事件先在input上触发响应,然后才会冒泡到div上,以下代码会先输出input
,后输出div
,所捕获不同的是,addEventListener
的第三个参数为false
。
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('pop-div');
}, false);
input.addEventListener('click', (e) => {
alert('pop-input');
}, false);
事件流
有聪明的朋友马上就会问:“如果我同时绑定了捕获事件和冒泡事件呢”?
<div>
<input type="button" value="test" />
</div>
如果在按钮上点击,事件先在input上触发响应,然后才会冒泡到div上,以下代码会先输出input
,后输出div
,所捕获不同的是,addEventListener
的第三个参数为false
。
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('capture-div');
}, true);
input.addEventListener('click', (e) => {
alert('capture-input');
}, true);
div.addEventListener('click', (e) => {
alert('pop-div');
}, false);
input.addEventListener('click', (e) => {
alert('pop-input');
}, false);
它们的响应顺序为
capture-div
capture-input
pop-input
pop-div
阻止捕获/冒泡
有的时候,我们希望点击事件再传播,就需要将其阻止,如下例,我们希望事件不再向上冒泡,使用e.stopPropagation();
之后,果然div上面的响应不再被触发。
<div>
<input type="button" value="test" />
</div>
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('pop-div');
}, false);
input.addEventListener('click', (e) => {
alert('pop-input');
e.stopPropagation();
}, false);
如果是捕获事件呢?在前文中我没有说阻止事件冒泡,而是说阻止事件传播,是因为方法stopPropagation
不仅仅是用来阻止冒泡而已,看下例:
<div>
<input type="button" value="test" />
</div>
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('pop-div');
e.stopPropagation();
}, true);
input.addEventListener('click', (e) => {
alert('pop-input');
}, true);
同样的,事件只在div节点上被触发了一次。
那么,是不是stopPropagation
能够解决我们所有的问题了呢?答案是否定的,看下例
<div>
<input type="button" value="test" />
</div>
const div = document.querySelector('div');
const input = document.querySelector('input');
div.addEventListener('click', (e) => {
alert('capture-div');
}, true);
input.addEventListener('click', (e) => {
alert('capture-input');
e.stopPropagation();
}, true);
input.addEventListener('click', (e) => {
alert('pop-input');
}, false);
input.addEventListener('click', (e) => {
alert('lalala');
}, false);
div.addEventListener('click', (e) => {
alert('pop-div');
}, false);
糟糕,在capture-input
之后,pop-input
,甚至lalala
都出来了,这也许不是我们想要的结果,怎么做呢,答案是将stopPropagation
换成stopImmediatePropagation
就可以了。那么方法stopPropagation
和stopImmediatePropagation
的差别是什么呢?从名字上看,就差了一个Immediate
,英国普通话讲,这个意思是立即,马上
的意思,加上Immediate
就马上停止了。实际上,stopImmediatePropagation
确实是让事件马上
停止了传播,而stopPropagation
则是到当前响应事件的dom节点为止,这其中的差异,不可不察。
委托
什么又是事件委托呢,事件委托是指利用事件模型,在外层的某个节点上绑定事件,而它内部的其它节点则委托这个节点
进行事件的响应处理。
例如
<ul>
<li data-value="1">test</li>
<li data-value="2">test</li>
<li data-value="3">test</li>
<li data-value="4">test</li>
<li data-value="5">test</li>
<li data-value="6">test</li>
<li data-value="7">test</li>
<li data-value="8">test</li>
<li data-value="9">test</li>
<li data-value="10">test</li>
</ul>
利用事件模型,我们只需要给外层的ul节点绑定一次事件即可,无须给每个li绑定事件
const ul = document.querySelector('ul');
ul.addEventListener('click', (e) => {
alert(e.target.dataset.value);
});
它的最佳使用场景是什么呢?就是现今移动端的列表页面。为什么呢?因为列表中的某一行的事件,如果交由每一行去做,监听的事件太多了,会拖慢整个页面的响应速度,某些低端的android机性能可能会是很差的。但这还不是最要命的,要命的是,列表里的内容是不固定的,通常我们是会在列表快要到底部时再加载更多内容,甚至在某些对性能要求非常严格的场景下,我们需要移除最上面已经加载过了一些内容,防止一直加载更多导致列表过大性能下降。这个时候如果不使用委托,可以想像整个页面的性能和开发的复杂程度。使用委托,只需要给列表外层的某个节点绑定一次事件就搞定了,无论是从性能还是从开发的复杂程度上讲,都是最优的。