Event Loop 事件循环机制
Event Loop
Event Loop 也叫事件循环机制。
为解决 js 单线程而产生的异步问题,又出现了各种方式来实现异步:
- callback 回调函数;
- Promise:为解决回调地狱问题应运而生;
- Generator:通过yield关键字暂停和恢复函数的执行;
- async/await:Promise 和 Generator 的语法糖,解决 Promise.then 的链式调用,使代码看起来更像是同步代码
Event Loop 分为浏览器端和 nodejs 端,这里主要介绍的浏览器端的同步代码和异步代码的执行顺序问题。
宏任务和微任务
js 执行任务分为宏任务和微任务,宏任务放在任务队列,微任务放在微任务队列,先进先出(FIFO)。
微任务:Promise(then、catch、finally)、MutationObserver、Process.nextTick、defineProperty、Proxy;
宏任务:script、setTimeout、I/O、所有的网络请求
其中:defineProperty 和 Proxy 可被监听的数据拦截,本质上都是通过回调处理的,不是立刻执行的。
Promise
Promise 是同步代码,then、catch、finally 才是异步
new Promise(function (resolve) {
console.log('promise1') // 同步
resolve();
}).then(function () {
console.log('promise2') // 异步
})
async/await
await 之前的代码可以看做是同步的,await 右侧紧跟着的,也可以看作是同步的。await 下面的,被阻塞,整体看作是 Promise.then() 微任务
async function async1() {
console.log('async1 start') // 同步
await async2() // 同步
console.log('async1 end') // 异步
}
async function async2() {
console.log('async2')
}
Event Loop执行顺序
口诀:有微则微,无微则宏
- 先执行整个 script (宏任务),从而往下依次执行;
- 如果遇到同步任务直接进入主线程立即执行;如果遇到异步任务,宏任务放在任务队列,微任务放在微任务队列,待主线程空闲后,从任务队列中从前往后依次取出任务 tick 的回调函数进入主线程执行;
- 主线程清空后,先看当前层级有没有微任务,有的话就拿到主线程执行,直到清空微任务队列,再去执行下一层宏任务;
- 再下一层宏任务中,依旧按照上面的执行顺序,重复第2步和第3步。即先同步,清空后,再微任务,遇到宏任务,再加到下一轮,以此类推……
实战演练
有了上面了知识,来实操一下。
先看一个经典面试题:
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) { // S14
console.log('promise1')
resolve();
}).then(function () {
console.log('promise2')
})
console.log('script end')
这段代码的执行结果是什么?我们来分析一下:
-
首先遇到
async function async1
和async function async2
的函数定义,加了async
好像很厉害的样子,再厉害也遵循函数不调用就不执行的原则; -
接下来
console.log('script start');
直接控制台打印script start
;
控制台如下:
-
接着将
console.log( setTimeout )
的回调函数,在0秒后将放进任务队列,标记为 H1; -
接下来
async1()
调用函数后,开始执行async1
函数,控制台打印async1 start
,遇到await async2()
,await 后面的也是同步代码,控制台打印async2
,await 下面的console.log('async1 end')
,看做Promise.then( console.log('async1 end') )
,放进微任务队列,标记为 W1,此时async1()
处理结束;
控制台如下:
-
后面来到 S14 行(代码中有注释)
new Promise(function (resolve) { ... }
,控制台打印promise1
,遇到resolve()
,将 then 里面回调放进微任务队列,标记为 W2;
控制台如下:
-
接着来到最后一行
console.log('script end')
,控制台打印script end
控制台如下:
以上:第一轮宏任务执行完毕
-
接着要去看这轮宏任务中有无产生的微任务,很显然有 W1,W2,按先入先出原则,依次执行,控制台依次打印
async1 end
、promise2
,微任务队列清空;
控制台如下:
-
去看宏任务,作为第二轮宏任务执行,执行 H1,控制台打印
setTimeout
; -
至此,所有代码执行完毕,完整控制台如下:
小Tips
在 Chrome72 版本之前的版本,上面例子输出如下:
VM56:15 script start
VM56:2 async1 start
VM56:7 async2
VM56:9 promise3
VM56:21 promise1
VM56:26 script end
VM56:12 promise4
VM56:24 promise2
VM56:4 async1 end
undefined
VM56:17 setTimeout
async1 end
在 promise2
之后输出。
原因:在 Chrome72 版本之后浏览器 ECAM 规范有修改。
减少了 await 任务的循环次数,从而提高效能,通过直接调用 PromiseResolve() 代替之前的又一层 Promise 封装。