
一道经典的 JS 面试题弄懂事件循环

题目
1 | // 目标:回答代码执行顺序 |
最后两位尤其容易搞错,正确的答案是 1 7 5 6 2 3 4
。
为了搞懂上面这道题,先来看看相关知识。之后会有题目解析:
事件循环模型
基础
调用栈里存着程序源码,引擎会从上到下把其中的代码取到宿主环境里执行。
如果遇到异步任务,则会先塞进任务队列里,等所有同步代码都执行完,再来根据队列里的顺序或者情况一条条取出执行。
这里有一道题可以测试你的理解情况:
1 | /** |
正确答案是:1 5 3 2 4 6
宏任务和微任务
然而,ES6 之后引入了 Promise 对象, 让 JS 引擎也可以发起异步任务
异步任务划分为了两个:
- 宏任务:由浏览器环境执行的异步代码
- 微任务:由 JS 引擎环境执行的异步代码
微任务的优先级大于宏任务。
所以我们需要将队列拆分成两个,一个宏任务一个微任务:
宏任务 (Macrotasks)
可以理解为独立的、较大的工作单元。每次事件循环只执行一个。 常见的宏任务包括:
script
(整个脚本文件,可以看作是第一个宏任务)setTimeout()
和setInterval()
的回调函数I/O
操作(如文件读写)UI rendering
(浏览器渲染)- 用户交互事件(如
click
,scroll
)
微任务 (Microtasks)
可以理解为需要尽快执行的、与当前任务紧密相关的小任务。在一个宏任务执行完毕后,会立即执行所有微任务。 常见的微任务包括:
Promise.then()
,Promise.catch()
,Promise.finally()
的回调函数await
后面的代码(因为async/await
是基于 Promise 的语法糖)queueMicrotask()
MutationObserver
题目解析
微任务队列有两个特点:
- 优先级大于宏任务队列。
- 每一条宏任务执行完,都会清空微任务队列,才会继续执行下一条宏任务。
这时候我们可以继续来看面试题了:
1 | // 目标:回答代码执行顺序 |
过程
我们可以分析一下程序执行过程:
-
首先,程序里的同步代码肯定是先执行完,所以先输出了 1、7,其他的代码塞进了队列里。
-
微任务队列里的优先级更高,所以先处理微任务队列。微任务队列里现在应该有两条语句:
1 | p.then(result => console.log(result)) |
而且里面的 result 值都已经有了,这是因为一旦 promise 里的 resolve 被执行,那就会调用后面的 then。
这两条语句按顺序执行,那就是 5、6
。
- 这时候开始看宏任务队列。第一个需要解决的任务是:
1 | // 1️⃣ |
所以先输出了一个 2
,之后又看到有 then()
语句,塞入微任务队列。
- 重点来了。第一个宏任务队列里的任务执行完了,并不会继续解决第二个宏任务队列里的任务,而是会看一眼微任务队列有没有东西。
结果发现有一个(第三步里塞的),那就执行。于是输出 3
。
- 既然微任务队列再次被清空了,那就从宏任务队列里取出第二个任务:
1 | setTimeout(() => { |
执行,所以输出 4
。到这里,所有任务都执行完毕了。
注意点
-
上面的第四点很关键,它是事件循环的核心机制,也是新人容易弄错的地方。
-
还有一个比较容易出问题的点:promise 内部的代码总在 promise 实例创建的时候就执行,所以可以理解为是同步代码。
总结
事件循环可以用下面的流程概括:
- 先执行完所有同步代码。
- 然后清空所有的微任务 (Microtask)。
- 接着取一个 宏任务 (Macrotask) 来执行。
- 执行完这个宏任务后,再回到第 2 步,检查并清空所有微任务。
- 不断重复这个循环。
这个机制确保了高优先级的微任务(如 Promise.then
)总是在下一个宏任务(如 setTimeout
或用户点击)之前被执行。
- 标题: 一道经典的 JS 面试题弄懂事件循环
- 作者: 三葉Leaves
- 创建于 : 2025-06-17 00:00:00
- 更新于 : 2025-07-10 13:40:50
- 链接: https://blog.oksanye.com/e542963c65f3/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。