JS篇-事件流

142 阅读4分钟

事件流

要说事件流,首先要提到的肯定是事件。我们在软件领域,尤其在前端,事件是大多情况下其实可以理解为用户与应用之间的交互。比如我们经常会在业务中用到的 onclickonChange 事件等。在传统软件工程领域,这个模型叫做“观察者模式”。

拿点击事件为例,当我们从点击触发事件开始到事件结束,这个过程中都经历了些什么呢?我们来看一张图:\

如图,我们现在有一个嵌套三层 div 的盒子:

  • root——最外层
  • father——中间层
  • child——最里层

此时我们需要在 child 的元素上进行点击,点击的操作会透过 rootfather 两个外层的元素到达 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 节点上,接收三个参数:

  1. 事件名
  2. 事件处理函数
  1. 布尔值
    1. true:捕获阶段调用事件处理程序
    2. false(默认值):冒泡阶段调用事件处理程序

⚠️注意:event 对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁掉