手记

浅析事件捕获和事件冒泡

前言

作为一名前端开发工作者,肯定绕不开页面的事件流。刚入门的程序员,可能会因为在页面中点击事件触发了父元素或者子元素事件而苦恼,却往往不了解其中的机制。这一篇来简单讲一讲事件捕获和事件冒泡。

解决什么?

追究历史,事件冒泡事件捕获分别由微软网景公司提出,目的在于解决页面中的事件流问题,即元素间事件触发的时序。当然,分久必合,在微软和网景之间火热争论之后,最后采用了 W3C 的折中方案——先捕获后冒泡。当然这个会在下文中详细的讲。

思考问题

首先从一个小例子进行引入,假设有一个页面,结构上从外到内分别是:

  1. div #container

  2. div #btnContainer

  3. button #btn


<div  id="container">

<div  id="btnContainer">

<button  id="btn">点击按钮</button>

</div>

</div>

这三个元素呈嵌套关系,我们分别为三个元素赋予点击事件:


const  container  =  document.getElementById('container');

const  btnContainer  =  document.getElementById('btnContainer');

const  btn  =  document.getElementById('btn');

  

// window 绑定事件

window.addEventListener('click', () => {

console.log('win')

})

  

document.addEventListener('click', () => {

console.log('doc');

});

  

container.addEventListener('click', () => {

console.log('container');

});

btnContainer.addEventListener('click', () => {

console.log('btnContainer');

});

btn.addEventListener('click', () => {

console.log('btn');

});

那么,点击按钮button会打印什么样的信息呢?

通过浏览器打开页面,我们可以看到,控制台上打印的 btn -> btnContainer -> container -> doc -> win 。

这说明,浏览器的事件触发会有一个传递过程,并且父子相对之间是有顺序的。上面的例子所呈现的现象,正是事件冒泡

事件冒泡

事件冒泡很好理解,简单理解正如水里冒泡一般,从内往外冒。在我们的程序中,事件冒泡会从我们的 target 对象,即当前操作的对象,开始一层一层往外走,直到 document,最后是 window 对象。根据上面的例子画成流程图如下:

@flowstart

stage1=>operation: btn

stage2=>operation: btnContainer

stage3=>operation: container

stage4=>operation: document

stage5=>operation: window

stage1->stage2->stage3->stage4->stage5

@flowend

事件捕获

事件捕获正好和事件冒泡是相反的,事件冒泡在于往外走,而事件捕获则是从 window 对象开始,一层层往内走,直到当前 target 对象上。

相应的,我们的流程图是这样:

@flowstart

stage5=>operation: btn

stage4=>operation: btnContainer

stage3=>operation: container

stage2=>operation: document

stage1=>operation: window

stage1->stage2->stage3->stage4->stage5

@flowend

EventTarget.addEventListener

了解了事件捕获和事件冒泡之后,我们来讲一讲如何处理事件。

了解 API

由于在页面中,事件呈现流一样的形式层层传递,因此我们需要在某一阶段中进行事件的处理。首先看看 EventTarget.addEventListener 方法:

target.addEventListener(type, listener [, useCapture]);

可以看见,该方法接受三个参数,分别是事件类型、侦听器和是否使用事件捕获

其中, useCapture 默认是 false,即事件句柄在冒泡阶段执行,因此在上文的例子中,我们可以清楚看到,控制台打印的顺序是从 btn 开始,直到 window 结束。

假如我们把上文例子中的 useCapture 都设置为 true, 那我们会看到的是这样的结果:

预料之中,这正好和冒泡是相反的。

先捕获后冒泡

事实上事件流是先进行捕获再进行冒泡的,我们常常会把事件处理分为三个阶段,即 事件捕获->目标阶段->事件冒泡

简单举个例子:


const  container  =  document.getElementById('container');

const  btnContainer  =  document.getElementById('btnContainer');

const  btn  =  document.getElementById('btn');

window.addEventListener('click', () => {

console.log('win')

})

  

document.addEventListener('click', () => {

console.log('doc')

})

  

container.addEventListener('click', () => {

console.log('container');

}, true);

btnContainer.addEventListener('click', () => {

console.log('btnContainer');

});

btn.addEventListener('click', () => {

console.log('btn');

});

如上,我们给 container 绑定事件设置了使用捕获事件,打印结果变成了:

很明显看到,container 被打印在了第一个位置,这正好也验证了先捕获后冒泡的规则。

阻止传播

有时候,我们只想要触发当前元素即可,并不希望把事件传播到上层或者下层元素,那么这个时候我们就需要设置阻止传播了,而通用的做法是添加 event.stopPropagation();

还是用上文的例子,这一次我们在 btn 元素上添加阻止的代码。


const  container  =  document.getElementById('container');

const  btnContainer  =  document.getElementById('btnContainer');

const  btn  =  document.getElementById('btn');

window.addEventListener('click', (event) => {

console.log('win')

})

  

document.addEventListener('click', () => {

console.log('doc')

})

  

container.addEventListener('click', () => {

console.log('container');

});

btnContainer.addEventListener('click', () => {

console.log('btnContainer');

});

btn.addEventListener('click', (event) => {

event.stopPropagation();

console.log('btn');

});

查看控制台,效果如下:

可以看到,event.stopPropagation(); 阻止了冒泡,事件在 btn 的时候,就不会继续传播下去了。

而相应的,在事件捕获阶段也可以使用 event.stopPropagation();, 只不过情况有所不同, 捕获阶段阻止的是从 windowtarget 的传播路径的某一环节。

事件代理

接下来来思考这样一个场景,假设我们操纵一个列表,要求点击某一项的时候,会把该项的内容在指定的元素上显示出来。那么,我们可以捏造一个内容结构:


<div  id="container">

<div  class="show"></div>

<ul  id="list">

<li>这是第一个li</li>

<li>这是第二个li</li>

<li>这是第三个li</li>

<li>这是第四个li</li>

<li>这是第五个li</li>

</ul>

</div>

假设我们点击 li 的时候,需要将该元素的内容显示到 div.show 上。

那么我们要怎么做呢?我们当然可以给每一项都添加事件。


const  lis  =  Array.from(document.getElementsByTagName('li'));

const  show  =  document.getElementsByClassName('show')[0];

lis.forEach(element  => {

element.addEventListener('click', (e) => {

const  target  =  e.target

show.innerHTML  =  target.innerHTML

})

});

查看效果:

可以发现功能是可以实现的。

但是,这样的做法也存在着弊端,比如:

  • 每一个 li 都要去绑定一个事件,消耗了内存。

  • 动态添加或者删除 li 的时候,需要重新添加或者清除事件。

因此,解决这个问题,我们常常会用到一个做法,就是事件委托。事件委托本质上就是把子元素的事件委托给父元素来处理,由于页面的事件流特性,我们可以利用冒泡的特性来编码:


const  show  =  document.getElementsByClassName('show')[0];

list.addEventListener('click', (e) => {

const  target  =  e.target

if (target.nodeName.toLowerCase()==="li") {

show.innerHTML  =  target.innerHTML

}

})

打开浏览器看看效果:

可以看到,通过事件委托的方式来代理批量子元素的做法是完全可取的。

总结

  • 事件捕获和事件冒泡主要解决了页面事件流的问题。统一后,页面的事件流经过了三个阶段,分别是事件捕获、目标阶段和事件冒泡阶段。

  • 事件捕获是从 window 对象开始,一层层往内走,直到当前 target;而事件冒泡则是相反。

  • event.stopPropagation() 可以阻止事件流的进一步传播。

  • 采用事件代理的方式,能够节省内存消耗,对于动态改变子元素的时候,也非常有利,避免了很多麻烦的步骤,比如重新绑定事件。

在线阅读

1人推荐
随时随地看视频
慕课网APP