事件流
要说事件流,首先要提到的肯定是事件。我们在软件领域,尤其在前端,事件是大多情况下其实可以理解为用户与应用之间的交互。比如我们经常会在业务中用到的 onclick、onChange 事件等。在传统软件工程领域,这个模型叫做“观察者模式”。
拿点击事件为例,当我们从点击触发事件开始到事件结束,这个过程中都经历了些什么呢?我们来看一张图:\
如图,我们现在有一个嵌套三层 div 的盒子:
- root——最外层
- father——中间层
- child——最里层
此时我们需要在 child 的元素上进行点击,点击的操作会透过 root 和 father 两个外层的元素到达 child。我们将这个过程称之为捕获——即从外层元素开始到寻找出需要被操作的元素。
那捕获到的元素我们就称之为目标。
而当我们的流程走到目标元素时,又会从目标元素从里向外走,路过 father 走到 root,我们将这个过程称之为冒泡。最终形成了一个闭环,我们将这一过程称之为事件流。
那到这里我们就可以理解事件流的本质——其实就是描述了页面接收事件的顺序。
那上述提到的三个名词其实在 DOM2 Events 规范里也对应了:
- 事件捕获
- 到达目标
- 事件冒泡
事件捕获
捕获阶段是从外层元素到目标元素的过程,这为提供拦截事件提供了可能。
纵观每个点击事件,其实都是会从 document 对象开始进行捕获事件,然后沿着 DOM 树依次向下传播,直到达到目标元素。
实际上,现在所有浏览器都是从 windows 对象开始捕获事件,而 DOM2 Event 规范规定的是从 document 开始。但是又由于旧版浏览器不支持,所以实际当中几乎不怎么使用事件捕获。通常建议使用事件冒泡,特殊情况下可以使用事件捕获。
目标阶段
在目标阶段,实际的目标元素接收到了事件。
事件冒泡
最后到达冒泡阶段,这是最迟响应事件的阶段。
和事件捕获的方向相反,捕获是从外到里,而冒泡是从里向外。所有浏览器都是支持事件冒泡的(PS: IE8 及更早版本不支持),只不过在实现方式上会有一些变化。现代浏览器中的事件会一直冒泡到 window 对象。
假如我们需要阻止事件流在DOM结构中的传播,可以使用 stopPropagation() 方法,从而取消后续的捕获和冒泡。
let btn = document.getElementById('btn');
btn.onclick = function (event) {
console.log('Clicked');
event.stopPropagation();
}
document.body.onclick = function (event) {
console.log('body clicked');
}
如果这个例子不调用里面的 stopPropagation 方法,那么点击按钮就会打印出两条日志。但是调用后就会阻止掉后续的事件冒泡,从而导致 click 事件不会传播到 document.body 上去,这样的话 document.body 上的 onclick 事件处理程序就永远不会执行。
如何判断当前事件是处于哪个阶段呢?答案是——eventPhase 属性。它有以下对应关系:
eventPhase: 1 —— 捕获阶段eventPhase: 2 —— 目标阶段
eventPhase: 3 —— 冒泡阶段
⚠️注意:目标阶段通常在事件处理时会被认为是冒泡阶段的一部分
事件处理程序中的事件流
我们现在常用的事件处理程序有可能是 DOM0 的,也有可能是 DOM2 的。对于这两种不同的事件处理程序,所对应的事件流阶段也是不一样的。
DOM0
let btn = document.getElementById('btn');
btn.onclick = function() {
console.log('Clicked')
}
在这里我们把一个函数赋值给 DOM 元素的一个事件处理程序属性。而以这种方式添加事件处理程序是注册在事件流的冒泡阶段的(实际在目标节点上)。
DOM2
let btn = document.getElementById('btn');
btn.onclick = function (event) {
console.log(event.eventPhase); // 2 目标上
}
document.body.addEventListener('click', (event) => {
console.log(event.eventPhase); // 1
}, true)
document.body.onclick = function (event) {
console.log(event.eventPhase); // 3
}
DOM2 中新增了 addEventListener 方法,这个方法暴露在了所有的 DOM 节点上,接收三个参数:
- 事件名
- 事件处理函数
- 布尔值
-
true:捕获阶段调用事件处理程序false(默认值):冒泡阶段调用事件处理程序
⚠️注意:event 对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁掉