DOM 事件模型——捕获和冒泡

296 阅读4分钟

在用户浏览网页时,我们常常需要页面对用户的操作作出实时的响应,比如点击“阅读全文”后我们期望页面展示被折叠的文本,按下回车键后浏览器提交用户填好的文本内容。用户的各种操作都是“事件”。此外,也有系统触发的一些事件,也需要DOM元素作出相应的响应。

基本事件

而在JavaScript中,事件在未得到W3C组织标准化之前,各浏览器就有一个事件模型 —— 基本事件模型(Basic Event Model)。比如最简单的,我们给button按钮绑定一个click事件:

<button onclick="console.log('点击!')"></button>

这是内联式的写法,也可在js脚本中来写

let button = document.querySelector('button');
button.onclick = () => console.log('点击!');

形如这样的事件写法,就是基本事件模型。在基本事件模型中,绑定一个浏览器预设支持的事件,在该事件发生时,调用指定的函数,让事件触发后面指定的执行语句。像这样的做法,也被称为传统模型传统注册模型

这类模型有一个典型的缺点,就是只能注册一个事件处理响应,如果想给同一元素注册多个事件响应是不行的。如果给一个元素注册多个事件,后来的事件就会覆盖前面的事件,最后只会触发最后一个注册成功的事件进行响应。

addEventListener监听

而使用监听的方法来操作事件会更好的控制事件的响应,也能给事件绑定多个响应。

EventTarget.addEventListener()方法将指定的监听器注册到EventTarget上,当该对象触发指定的事件时,指定的响应函数就会被执行。EventTarget可以是element对象,document对象或者任何其他支持监听事件的对象。如:

<button></button>

<script>
let button=document.querySelector('button');
button.addEventListener('click',function(e){console.log('点击');});
button.addEventListener('click',function(e){console.log(e);});
</script>

function中的e,用来接受浏览器监听到事件时给你传递的参数,一个事件对象。可以看到,上文给一个事件同时绑定了两个响应函数。

在使用addEventListener进行事件监听时需要先理清浏览器是如何触发响应的。上面的例子都是在一个button按钮上进行绑定,非常清晰。那么如果我们监听一个被多层嵌套的元素的click事件,用户点击该元素后,它的父元素是不是也触发了点击?那么事件响应的执行顺序是如何决定的呢?这就引入了今天的重点概念,DOM事件流——捕获和冒泡。

DOM事件流——捕获和冒泡

说起捕获和冒泡,可能会没法得到一个直观的印象。这两个说法还不够形象。结合图示可能就更好理解了。首先来看一张示意图:

image.png

图片来源 w3.org

这时目前最常用的DOM事件模型,现代浏览器都支持此模型。在此事件模型中,一次事件共有三个过程:

  1. 事件捕获阶段(Capturing Phase) :事件从 document 一直向下查询、传递到目标元素,依次检查经历过的节点是否绑定了事处监听函数(事件处理程序),如果有则执行,反之不执行

  2. 事件处理阶段(Target Phase) :事件到达目标元素,触发目标元素的监听函数

  3. 事件冒泡阶段(Bubbling Phase) :事件从目标元素反过来逐层冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行,反之不执行。

简言之,事件一开始从文档的根节点流向目标对象(捕获阶段),然后在目标元素上被触发(目标阶段),之后再回溯到文档的根节点(冒泡阶段)。

关于浏览器对于事件的捕获和冒泡处理,其实还有一件趣闻。当年微软公司和网景公司在浏览器行业竞争激烈,两家有些针锋相对,网景公司坚持对事件采用捕获的机制,而微软则是采用冒泡的方案,两家谁也说服不了谁,压力来到了制定标准的W3C组织这边。W3C出面做了和事佬,考虑兼容性,结合了两家的方案,规定浏览器必须先进行捕获后再进行冒泡,这看起来似乎是重复劳动了,但给了你选择的自由,你可以将事件响应绑定在捕获或冒泡阶段,他们分别有自己的特点。

不过,我们最常用的方法还是冒泡法,addEventListener()默认就会将响应绑定在冒泡阶段。()中第三项参数为true,是绑定在捕获阶段,它不填默认就为false,绑定在冒泡阶段。两者最大的区别是,捕获的触发响应顺序是由外向内,而冒泡的触发顺序是由内而外。

补充一句,在DOM事件模型中,如果要移除事件处理程序,可以使用removeEventListener()方法。