【前端面试题】理清JavaScript事件循环(Event Loop)
为什么 JavaScript 是单线程?
作为浏览器端脚本语言,JS 主要用途是与用户交互,操作 DOM,发送网络请求等。这用途决定了单线程更合适,否则会带来很复杂的同步问题。因此为了避免复杂性,JavaScript 就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
如何解决并发问题呢?
- JavaScript 使用异步来解决 I/O 的并发场景。
- Runtime 通过 Web Worker、Child process(NodeJS) 等方式可以创建多线程(进程)来充分利用多核 CPU;
- Event Loop 是实现异步 I/O 的一种方案(不唯一)。
拓展小知识:
并发(Concurrent),在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
并行(Parallel),当系统有一个以上 CPU 时,当一个 CPU 执行一个进程时,另一个 CPU 可以执行另一个进程,两个进程互不抢占 CPU 资源,可以同时进行,这种方式我们称之为并行(Parallel)。
它们区别有哪些呢?
并发,指的是多个事情,在同一时间段内同时发生了。并发的多个任务之间是互相抢占资源的。
并行,指的是多个事情,在同一时间点上同时发生了。并行的多个任务之间是不互相抢占资源的。
只有在多 CPU 的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。
事件循环
-
JavaScript 虽然是单线程,Engine 维护了一个栈用于执行进栈的任务;执行任务的过程可能会调用一些 Runtime 提供的异步接口。
-
Runtime 等待异步任务(如定时器、Promise、文件 I/O、网络请求等)完成后会把 callback 扔到 Task Queue(如定时器)或 Microtask Queue(如 Promise);
-
JavaScript 主线程栈空了后 Microtask Queue 的任务会依次扔到栈里执行,直到清空,之后会取出一个 Task Queue 里可以执行的任务扔到栈里执行;
-
周而复始。
事件循环是通过 「任务队列」 的机制来进行协调的。一个 Event Loop 中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合;每个任务都有一个任务源(task source),「源自同一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列」。 setTimeout/Promise 等 API 便是任务源,而进入任务队列的是他们指定的具体执行任务。
setTimeout/Promise 等 API 便是任务源,而进入任务队列的是他们指定的具体执行任务。
JS 中任务分为两类:
宏任务:是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得 JS 内部(macrotask)与 DOM 任务能够有序的执行,会在一个(macrotask)执行结束后,在下一个(macrotask) 执行开始前,对页面进行重新渲染。
微任务:可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。
所以它的响应速度相比 setTimeout(setTimeout 是 task)会更快,因为无需等渲染。也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。
在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;
宏任务和微任务执行流程:
- 整段脚本 script 作为宏任务开始执行
- 遇到微任务将其推入微任务队列,宏任务推入宏任务队列
- 一个宏任务执行完毕,检查有没有可执行的微任务
- 发现有可执行的微任务,将所有微任务执行完毕
- 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
- 渲染完毕后,JS 线程继续接管,开始新的宏任务,反复如此直到所有任务执行完毕
总结起来的执行的循环顺序如下:
宏任务(一个) --> 微任务(所有) --> Dom 渲染 --> 宏任务
任务类型 | 任务源 |
---|---|
宏任务(macrotask ) | 整段脚本script , setTimeout , setInterval ,I/O , UI交互事件 , postMessage , MessageChannel , setImmediate(Node.js 环境) |
微任务(microtask ) | promise.then catch finally , process.nextTick (Node 使用) MutationObserver (浏览器使用) |
注意点
-
Promise 和 async 中的立即执行
我们知道 Promise 中的异步体现在 then 和 catch 中,所以写在 Promise 中的代码是被当做同步任务立即执行的。而在 async/await 中,在出现 await 出现之前,其中的代码也是立即执行的。那么出现了 await 时候发生了什么呢? -
await 做了什么?
从字面意思上看 await 就是等待,await 等待的是一个表达式,这个表达式的返回值可以是一个 promise 对象也可以是其他值。
很多人以为 await 会一直等待之后的表达式执行完之后才会继续执行后面的代码,「实际上 await 是一个让出线程的标志」。
await 紧跟后面的表达式会先执行一遍,将 await 后面的代码加入到 microtask 中,然后就会跳出整个 async 函数来执行后面的代码。
由于因为 async await 本身就是 promise+generator 的语法糖。所以 await 后面的代码是 microtask。
面试解题小技巧
- 脑中总是想着一个当前执行栈,当前脚本作为开始的宏任务,然后将遇到关键词(见上面总结)分配到不同的任务队列中。 执行栈完成后,取出来全部微队列(在微队列放入栈中执行时,可能会产出新的微队列,依然要取),微队列彻底为空时才去执行宏队列。
- 有些面试比较坑,
setTimeout
后边有有个时间。会影响到输出顺序。 - 见过比较好的,答题同学,是将当前执行队列在代码后边标注一下,方便记录执行到哪里了。 这种方法非常棒。
来看一些例子
- 热身题目, 宏队列和微队列的交替执行过程
setTimeout(() => {
console.log('setTimeout')
}, 0)
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => {
console.log('promise3')
Promise.resolve().then(() => {
console.log('promise4')
})
})
})
console.log('script end')
结果为:promise2, script end, promise2.then, promise3, promise4, setTimeout
- 题目 1
console.log('a')
setTimeout(function(){
console.log('b')
}, 200)
setTimeout(function(){
console.log('c')
}, 0)
console.log('d')
数据结果为: a d b c
- 题目 2
这道题其实是很经典的,包括如何把下面的输出给改对,经常被问到。
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i)
}, 1000)
}
结果:十个 10
每次 for 循环遇到 setTimeout 都将其放入事件队列中等待执行,直到全部循环结束,i 作为全局变量当循环结束后 i = 10 ,再来执行 setTimeout 时 i 的值已经为 10 , 结果为十个 10。
- 题目 3
const p = new Promise(resolve => {
console.log('a')
resolve()
console.log('b')
})
p.then(() => {
console.log('c')
})
console.log('d')
结果是 a b d c
。 Promise 中的函数相当于在当前队列中因此首先会被执行所以 a
b
会被输出,紧接着当前会将then
放在微队列中等待执行,但是当前队列中还有d
被输出。
- 题目 4
setTimeout(function(){
console.log('setTimeout')
}, 0)
const p = new Promise(resolve => {
console.log('a')
resolve()
console.log('b')
})
p.then(() => {
console.log('c')
setTimeout(function(){
console.log('then中的setTimeout')
}, 0)
})
console.log('d')
结果为 a b d c setTimeout then中的setTimeout
- 题目 5
console.log('a');
new Promise(resolve => {
console.log('b')
resolve()
}).then(() => {
console.log('c')
setTimeout(() => { //macro----2
console.log('d')
}, 0)
})
setTimeout(() => { //macro ----3
console.log('e')
new Promise(resolve => {
console.log('f')
resolve()
}).then(() => {
console.log('g')
})
}, 100)
setTimeout(() => {
console.log('h')
new Promise(resolve => {
resolve()
}).then(() => {
console.log('i')
})
console.log('j')
}, 0)
结果为:a, b, c, h, j ,i, d, e, f, g
。 注意中间 setTimeout 的时间间隔
- 题目 6
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
结果为:script start, async1 start, async2, promise1, script end, async1 end, promise2, setTimeout
变形题:
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
//async2做出如下更改:
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
}
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
console.log('promise3');
resolve();
}).then(function () { //micro ----3
console.log('promise4');
});
console.log('script end');
结果为: script start, async1 start, promise1,promise3, script end, promise2 , async1 end, promise4, setTimeout
变形题:
async function async1() {
console.log('async1 start')
await async2()
// 更改如下:
setTimeout(function () { // macro ----- 3
console.log('setTimeout1')
}, 0)
}
async function async2() { // macro ----- 2
// 更改如下:
setTimeout(function () {
console.log('setTimeout2')
}, 0)
}
console.log('script start')
setTimeout(function () { // macro ---- 1
console.log('setTimeout3')
}, 0)
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
结果为: script start, async1 start, promise1, script end, promise2, setTimeout3, setTimeout2, setTimeout1
async function a1() {
console.log('a1 start')
await a2()
console.log('a1 end')
}
async function a2() {
console.log('a2')
}
console.log('script start')
setTimeout(() => { // macro ---1
console.log('setTimeout')
}, 0)
Promise.resolve().then(() => {
console.log('promise1')
})
a1()
let promise2 = new Promise((resolve) => {
resolve('promise2.then')
console.log('promise2')
})
promise2.then((res) => {
console.log(res)
Promise.resolve().then(() => { // micro --- 1
console.log('promise3')
})
})
console.log('script end')
结果为:script start, a1 start, a2, promise2, script end, promise1, a1 end, promise2.then, promise3, setTimeout