11.3 浏览器中的 Event Loop
11.3 浏览器中的 Event Loop
问题一:异步代码执行顺序?解释一下什么是 Event Loop ?
上一小节我们讲到了什么是执行栈,大家也知道了当我们执行 JS 代码的时候其实就是往执行栈中放入函数,那么遇到异步代码的时候该怎么办?其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
[图片上传失败...(image-b2db90-1573526269808)]
不同的任务源会被分配到不同的 Task 队列中,任务源可以分为微任务(microtask) 和宏任务(macrotask)。在 ES6 规范中,microtask 称为jobs
,macrotask 称为task
。下面来看以下代码的执行顺序:
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
首先先来解释下上述代码的async
和await
的执行顺序。当我们调用async1
函数时,会马上输出async2 end
,并且函数返回一个Promise
,接下来在遇到await
的时候会就让出线程开始执行async1
外的代码,所以我们完全可以把await
看成是让出线程的标志。
然后当同步代码全部执行完毕以后,就会去执行所有的异步代码,那么又会回到await
的位置执行返回的Promise
的resolve
函数,这又会把resolve
丢到微任务队列中,接下来去执行then
中的回调,当两个then
中的回调全部执行完毕以后,又会回到await
的位置处理返回值,这时候你可以看成是Promise.resolve(返回值).then()
,然后await
后的代码全部被包裹进了then
的回调中,所以console.log('async1 end')
会优先执行于setTimeout
。
如果你觉得上面这段解释还是有点绕,那么我把async
的这两个函数改造成你一定能理解的代码
new Promise((resolve, reject) => {
console.log('async2 end')
// Promise.resolve() 将代码插入微任务队列尾部
// resolve 再次插入微任务队列尾部
resolve(Promise.resolve())
}).then(() => {
console.log('async1 end')
})
也就是说,如果await
后面跟着Promise
的话,async1 end
需要等待三个 tick 才能执行到。那么其实这个性能相对来说还是略慢的,所以 V8 团队借鉴了 Node 8 中的一个 Bug,在引擎底层将三次 tick 减少到了二次 tick。但是这种做法其实是违法了规范的,当然规范也是可以更改的,这是 V8 团队的一个PR,目前已被同意这种做法。
所以 Event Loop 执行顺序如下所示:
- 首先执行同步代码,这属于宏任务
- 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
- 执行所有微任务
- 当执行完所有微任务后,如有必要会渲染页面
- 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是
setTimeout
中的回调函数
所以以上代码虽然setTimeout
写在Promise
之前,但是因为Promise
属于微任务而setTimeout
属于宏任务,所以会有以上的打印。
微任务包括process.nextTick
,promise
,MutationObserver
。
宏任务包括script
,setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
。
这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了script
,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。