likes
comments
collection
share

一道面试题带你了解事件流和Event Loop

作者站长头像
站长
· 阅读数 7

最近在网上看到一道面试题,起初我觉得就是一道比较简单的,但是仔细研究一下发现其中有很多考点,并且可以延伸和拓展很多知识点,那么让我们一起来看看这道面试题,并且梳理一下相关的知识点吧。话不多说,看题:

面试题

document.body.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(1))
  console.log(2);
}, false);
document.body.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(3))
  console.log(4);
}, false);
其输出结果是什么?

事件流

当一个节点产生一个事件时,该事件会在元素结点与根节点之间按照特定的顺序传播,传播经过的所有节点都会接受到该事件,这个传播过程我们称之为DOM事件流。

事件流分为三个阶段: 1.捕获阶段2.当前目标阶段3.冒泡阶段

事件冒泡: 事件开始时有具体的元素接收,然后逐级向上传播到DOM最顶层结点过程

事件捕获: 由DOM最顶层节点开始,然后逐级向下传播到最具体的元素接收的过程

目标阶段: 到达的具体元素

当我们给一个div注册一个点击事件时,其过程如下图:

一道面试题带你了解事件流和Event Loop

我们可以用一个具体的例子来看看事件流的实行结果:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>事件流案例</title>
  <style>
    #a{
      width: 400px;
      height: 400px;
      background: rgb(27, 100, 235);
    }
    #b{
      width: 200px;
      height: 200px;
      background: rgb(35, 225, 38);
    }
  </style>
</head>
<body>
  <div id="a">
    <div id="b"></div>
  </div>
  <script>
    let a = document.getElementById('a');
    let b = document.getElementById('b');
    a.addEventListener('click',() => {
      console.log('当前的目标阶段是a元素');
    })
    b.addEventListener('click',() => {
      console.log('当前的目标阶段是b元素');
    })
  </script>
</body>
</html>

当我们点击b容器时,首先点击到的是a容器,但是此时是冒泡事件,所以不会触发a容器的点击事件,事件流到b节点(目标节点)之后触发了目标节点的点击事件,然后开始向上层节点冒泡,将点击事件传播到a节点,此时是冒泡阶段触发了的a节点的点击事件。

当我们的addEventListener事件只写了两个参数时,第三个参数不写默认为false,也就是默认开启冒泡模式。

一道面试题带你了解事件流和Event Loop
  <script>
    let a = document.getElementById('a');
    let b = document.getElementById('b');
    a.addEventListener('click',() => {
      console.log('当前的目标阶段是a元素');
    },true)
    b.addEventListener('click',() => {
      console.log('当前的目标阶段是b元素');
    })
  </script>

但是如果我们将a容器开启捕获模式,再点击b容器,那打印结果就会是先打印a再打印b,因为捕获阶段是document --> html --> body --> a --> b

阻止冒泡的方法

event.stopPropagation()方法阻止捕获和冒泡阶段中当前事件的进一步传播。但是,它不能防止任何默认行为的发生;(阻止默认事件event.preventDefault())例如,对链接的点击仍会被处理。(我们想要从哪个节点开始阻止事件冒泡,就在那个节点添加上这个语句)这个方法存在版本兼容性问题,低版本的浏览器使用window.event.cancelBubble = true,即可以加一个判断语句

if(event && event.stopPropagation()){
     event.stopPropagation()
 } else {
     window.event.cancelBubble = true
 }

Event Loop

详细的细节可以看笔者的浅析宏任务、微任务和Event Loop这篇文章里面有宏任务微任务较为详细的介绍。这里我们就根据这道面试题直接讲最根本的内容,就是 addEventListener 属于I/O 是属于宏任务,每一个监听事件就相当于一次事件循环。

所以我们可以来解答一下开篇的这道面试题了。首先两个监听点击事件的第三个参数都为false,也就是默认开启冒泡模式,那就先执行第一个点击监听事件中的同步代码所以首先打印的是2,同步代码执行完之后找微任务队列,Promise.resolve()属于微任务队列,所以紧接着打印1。所以第一个事件循环就结束了,第二个addEventListener开启了第二次事件循环,同理所以先打印4,最后打印3。 所以开篇面试题执行的打印结果就是 2 --> 1 --> 4 --> 3

拓展题(1)

document.body.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(1))
  console.log(2);
}, false);
document.body.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(3))
  console.log(4);
}, true);

那如果我们将后面的点击监听事件改为捕获模式呢?这个时候由于都是在body容器里面,默认的执行顺序就是先捕获阶段 --> 目标阶段 --> 冒泡阶段,所以开启了捕获事件的监听事件优先执行,再根据宏任务微任务队列去执行,所以执行的打印结果就是 4 --> 3 --> 2 --> 1

拓展题(2)

document.body.addEventListener('click', () => {
  setTimeout(() => {
        console.log(1);
      })
  console.log(2);
}, false);
document.body.addEventListener('click', () => {
  setTimeout(() => {
        console.log(3);
      })
  console.log(4);
}, true);

它的执行结果就是 2 --> 4 --> 1 --> 3。

理解了的小伙伴可以在评论区告诉我你的见解,欢迎互动留言。希望这篇文章可以帮助到正在学习的你,有任何疑问或者错误,希望大佬们帮忙斧正,让我们一起进步。