飞道科技

飞道科技文档总汇

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);

它们的响应顺序为

  1. capture-div
  2. capture-input
  3. pop-input
  4. 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就可以了。那么方法stopPropagationstopImmediatePropagation的差别是什么呢?从名字上看,就差了一个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机性能可能会是很差的。但这还不是最要命的,要命的是,列表里的内容是不固定的,通常我们是会在列表快要到底部时再加载更多内容,甚至在某些对性能要求非常严格的场景下,我们需要移除最上面已经加载过了一些内容,防止一直加载更多导致列表过大性能下降。这个时候如果不使用委托,可以想像整个页面的性能和开发的复杂程度。使用委托,只需要给列表外层的某个节点绑定一次事件就搞定了,无论是从性能还是从开发的复杂程度上讲,都是最优的。