一道面试题带你了解事件流和Event Loop
最近在网上看到一道面试题,起初我觉得就是一道比较简单的,但是仔细研究一下发现其中有很多考点,并且可以延伸和拓展很多知识点,那么让我们一起来看看这道面试题,并且梳理一下相关的知识点吧。话不多说,看题:
面试题
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注册一个点击事件时,其过程如下图:

我们可以用一个具体的例子来看看事件流的实行结果:
<!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,也就是默认开启冒泡模式。

<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。
理解了的小伙伴可以在评论区告诉我你的见解,欢迎互动留言。希望这篇文章可以帮助到正在学习的你,有任何疑问或者错误,希望大佬们帮忙斧正,让我们一起进步。
转载自:https://juejin.cn/post/7185196286806032439