DOM事件传播机制及应用

如果有一个元素element1以及其子元素element2,两个元素都绑定了对应的事件,当我们点击element2的时候发现除了element2绑定的事件,element1绑定的事件也触发了。要怎么解决这个问题呢?恩,就是阻止事件冒泡。这里就涉及到了DOM的事件传播机制。

1
2
3
4
5
6
7
-----------------------------------
| element1 |
| ------------------------- |
| |element2 | |
| ------------------------- |
| |
-----------------------------------

事件模型

要了解DOM的事件传播机制,首先要了解DOM的以下三种事件模型。

  1. 由Netscape提出的 event capturing(事件捕获)
1
2
3
4
5
6
7
8
               | |
---------------| |-----------------
| element1 | | |
| -----------| |----------- |
| |element2 \ / | |
| ------------------------- |
| Event CAPTURING |
-----------------------------------

事件捕获的顺序是由外到内,所以element1的事件会先触发,然后element2的事件再触发。

  1. Microsoft主张的 event bubbling(事件冒泡)
1
2
3
4
5
6
7
8
               / \
---------------| |-----------------
| element1 | | |
| -----------| |----------- |
| |element2 | | | |
| ------------------------- |
| Event BUBBLING |
-----------------------------------

事件冒泡的顺序是由内到外,所以element2的事件会先触发,然后element1的事件再触发。

  1. W3C结合了上面两者的 W3C event model,也就是同时包含了事件捕获和事件冒泡
1
2
3
4
5
6
7
8
                 | |  / \
-----------------| |--| |-----------------
| element1 | | | | |
| -------------| |--| |----------- |
| |element2 \ / | | | |
| -------------------------------- |
| W3C event model |
------------------------------------------

由图中可以得出事件传播由三个阶段组成:

  1. 捕获阶段,事件由祖先元素到达目标元素
  2. 目标阶段,事件在目标元素触发
  3. 冒泡阶段,事件从目标元素回传到祖先元素

开发者可以通过addEventListener方法来绑定事件并且决定将事件是不是在捕获阶段执行,true为捕获阶段,false为冒泡阶段。 addEventListener()的第三个参数表示是否在捕获阶段调用事件处理程序,默认值是false,即在冒泡阶段执行事件。

如果你这么做

1
2
element1.addEventListener('click',doSomething2,true)
element2.addEventListener('click',doSomething,false)

当用户点击element2的时候,会按顺序发生以下事件:

  1. 开始事件捕获
  2. 到达element1,因为父元素element1调用addEventListener的第三个参数为true,所以捕获阶段中element1绑定的doSomething2执行
  3. 事件传播到达目标元素element2,element2绑定的doSomething执行
  4. 开始事件冒泡,因为父元素element1调用addEventListener的第三个参数为true,所以冒泡阶段没有任何事件执行

如果反过来

1
2
element1.addEventListener('click',doSomething2,false)
element2.addEventListener('click',doSomething,false)

当用户点击element2的时候,会按顺序发生以下事件:

  1. 开始事件捕获
  2. 到达element1,因为父元素element1调用addEventListener的第三个参数为false,所以捕获阶段中没有任何事件执行
  3. 事件传播到达目标元素element2,element2绑定的doSomething执行
  4. 开始事件冒泡,因为父元素element1调用addEventListener的第三个参数为false,所以冒泡阶段中element1绑定的doSomething2执行

应用

如何阻止事件冒泡

  1. stopPropagation
1
2
3
4
function doSomething(e) {
console.log('click')
if (e.stopPropagation) e.stopPropagation();
}

当我们调用stopPropagation的时候,阻止了事件冒泡到父元素,这就可以解决我们一开始提出的问题。

  1. stopImmediatePropagation

该方法的作用是,阻止事件冒泡并且阻止同类事件的其他侦听器被调用

如果我们给element2绑定如下两个click事件

1
2
3
4
5
6
7
8
function doSomething(e) {
console.log('click')
e.stopImmediatePropagation();
}

function doSomething3(e) {
console.log('click3')
}

结果是当我们点击元素的时候,doSomething会执行,而doSomething3不会执行。与stopPropagation的区别就在于 stopImmediatePropagation 除了阻止事件传播到父元素,还阻止了绑定在该元素的其他同类事件的执行。

事件委托

  • 减少内存消耗,提高页面性能,优化业务逻辑

假如我们现在有一个列表,每个项都要处理不一样的业务,我们可以给每个li元素绑定不同的函数,但我们知道函数是对象,会占用一定内存,这时候我们可以将事件绑定在li的父元素ul,然后利用事件冒泡,根据判断不同的target来执行不同的业务,减少内存的占用。

1
2
3
4
5
<ul id="father">
<li></li>
<li></li>
<li></li>
</ul>
1
2
3
4
document.getElementById('father').addEventListener('click', function (e) {
var target = e.target
if (target) ...
});
  • 动态绑定事件

现在的网页交互并不是简单的展示,很多时候我们都要根据数据动态的增删改dom。比如在上面的例子中,如果我们给每个li元素绑定事件的话,那么当我们删除li元素的话,对应的绑定事件也会丢失,如果后需新增li元素的时候也要我们再次绑定对应的事件。如果我们使用了事件委托,将事件绑定在父元素中,那将减少很多不必要的烦恼。

参考

Event order

EventTarget.addEventListener