首先要牢记一点: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 执行并打印
此时同步任务全部执行完毕,分别打印了 ‘script start’ 以及 ‘script end’,开始判断是否有微任务需要执行。
- 微任务队列存在任务,开始执行 then 回调函数
- 遇到 queueMicrotask,将回到加入微任务队列
- 遇到 console.log 执行并打印
检查发现微任务队列存在任务,执行 queueMicrotask 回调 - 遇到 console.log 执行并打印
此时发现微任务队列已经清空,判断是否需要进行 UI 渲染。
- 执行宏任务,开始执行 setTimeout 回调
- 遇到 console.log 执行并打印
执行一个宏任务即结束,寻找是否存在微任务,开始循环判断…
其实事件循环没啥难懂的,理解 JS 是个单线程语言,明白哪些是微宏任务、循环的顺序就好了。
最后需要注意的一点:正是因为 JS 是门单线程语言,只能同时执行一个任务。因此所有的任务都可能因为之前任务的执行时间过长而被延迟执行,尤其对于一些定时器而言。