var a = 0var b = async () => {a = a + await 10console.log('2', a) // -> ?}b()a++console.log('1', a) // -> ?
这道题目大部分读者肯定会想到 await 左边是异步代码,因此会先把同步代码执行完,此时 a 已经变成 1,所以答案应该是 11 。
其实 a 为 0 是因为加法运算法,先算左边再算右边,所以会把 0 固定下来 。如果我们把题目改成 await 10 + a 的话,答案就是 11 了 。
事件循环在开始讲事件循环之前,我们一定要牢记一点:JS 是一门单线程语言,在执行过程中永远只能同时执行一个任务,任何异步的调用都只是在模拟这个过程,或者说可以直接认为在 JS 中的异步就是延迟执行的同步代码 。另外别的什么 Web worker、浏览器提供的各种线程都不会影响这个点 。
大家应该都知道执行 JS 代码就是往执行栈里 push 函数(不知道的自己搜索吧),那么当遇到异步代码的时候会发生什么情况?
其实当遇到异步的代码时,只有当遇到 Task、Microtask 的时候才会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中 。
从图上我们得出两个疑问:
- 什么任务会被丢到 Microtask Queue 和 Task Queue 中?它们分别代表了什么?
- Event loop 是如何处理这些 task 的?
Task(宏任务):同步代码、setTimeout 回调、setInteval 回调、IO、UI 交互事件、postMessage、MessageChannel 。
MicroTask(微任务):Promise 状态改变以后的回调函数(then 函数执行,如果此时状态没变,回调只会被缓存,只有当状态改变,缓存的回调函数才会被丢到任务队列)、Mutation observer 回调函数、queueMicrotask 回调函数(新增的 API) 。
宏任务会被丢到下一次事件循环,并且宏任务队列每次只会执行一个任务 。
微任务会被丢到本次事件循环,并且微任务队列每次都会执行任务直到队列为空 。
假如每个微任务都会产生一个微任务,那么宏任务永远都不会被执行了 。
接下来我们来解决问题二 。
Event Loop 执行顺序如下所示:
- 执行同步代码
- 执行完所有同步代码后且执行栈为空,判断是否有微任务需要执行
- 执行所有微任务且微任务队列为空
- 是否有必要渲染页面
- 执行一个宏任务
console.log('script start');setTimeout(function() {console.log('setTimeout');}, 0);Promise.resolve().then(function() {queueMicrotask(() => console.log('queueMicrotask'))console.log('promise');});console.log('script end');
- 遇到 console.log 执行并打印
- 遇到 setTimeout,将回调加入宏任务队列
- 遇到 Promise.resolve(),此时状态已经改变,因此将 then 回调加入微任务队列
- 遇到 console.log 执行并打印
- 微任务队列存在任务,开始执行 then 回调函数
- 遇到 queueMicrotask,将回到加入微任务队列
- 遇到 console.log 执行并打印
- 检查发现微任务队列存在任务,执行 queueMicrotask 回调
- 遇到 console.log 执行并打印
- 执行宏任务,开始执行 setTimeout 回调
- 遇到 console.log 执行并打印
其实事件循环没啥难懂的,理解 JS 是个单线程语言,明白哪些是微宏任务、循环的顺序就好了 。
最后需要注意的一点:正是因为 JS 是门单线程语言,只能同时执行一个任务 。因此所有的任务都可能因为之前任务的执行时间过长而被延迟执行,尤其对于一些定时器而言 。
常见考点