面试季临近,Event Loop
这个概念也开始热了,博客上处处都在写,面试处处都在问,因而我也借此机会查阅了一些相关资料弥补本身的知识盲区,把本身学习完以后对于浏览器的 Event Loop
写一篇我的总结,有理解不对之处欢迎大佬指正~前端
这篇暂不作
Node
环境下的Event Loop
的讨论面试
在说 Event Loop
以前,咱们要先理解栈(stack
)和队列(queue
)的概念。chrome
栈和队列,二者都是线性结构,可是栈遵循的是后进先出(last in first off,LIFO
),开口封底。而队列遵循的是先进先出 (fisrt in first out,FIFO
),两头通透。segmentfault
Event Loop
得以顺利执行,它所依赖的容器环境,就和这两个概念有关。promise
咱们知道,在 js
代码执行过程当中,会生成一个当前环境的“执行上下文( 执行环境 / 做用域)”,用于存放当前环境中的变量,这个上下文环境被生成之后,就会被推入js
的执行栈。一旦执行完成,那么这个执行上下文就会被执行栈弹出,里面相关的变量会被销毁,在下一轮垃圾收集到来的时候,环境里的变量占据的内存就能得以释放。浏览器
这个执行栈,也能够理解为JavaScript
的单一线程,全部代码都跑在这个里面,以同步的方式依次执行,或者阻塞,这就是同步场景。bash
那么异步场景呢?显然就须要一个独立于“执行栈”以外的容器,专门管理这些异步的状态,因而在“主线程”、“执行栈”以外,有了一个 Task
的队列结构,专门用于管理异步逻辑。全部异步操做的回调,都会暂时被塞入这个队列。Event Loop
处在二者之间,扮演一个大管家的角色,它会以一个固定的时间间隔不断轮询,当它发现主线程空闲,就会去到 Task
队列里拿一个异步回调,把它塞入执行栈中执行,一段时间后,主线程执行完成,弹出上下文环境,再次空闲,Event Loop
又会执行一样的操做。。。依次循环,因而构成了一套完整的事件循环运行机制。异步
上图是笔者在 Google 上找的,比较简洁地描绘了整个过程,只不过其中多了
heap
(堆)的概念,堆和栈,简单来讲,堆是留给开发者分配的内存空间,而栈是原生编译器要使用的内存空间,两者独立。async
若是只想应付普通点的面试,上面一节的内容就足够了,可是想要答出下面的这条面试题,就必须再次深刻 Event Loop
,了解任务队列的深层原理:microtask
(微任务)和 macrotask
(宏任务)。函数
// 请给出下面这段代码执行后,log 的打印顺序
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')
// log 打印顺序:script start -> async2 end -> Promise -> script end -> promise1 -> promise2 -> async1 end -> setTimeout
复制代码
若是只有一个单一的 Task
队列,就不存在上面的顺序问题了。但事实状况是,浏览器会根据任务性质的不一样,将不一样的任务源塞入不一样的队列中,任务源能够分为微任务(microtask
) 和宏任务(macrotask
),介于浏览器对两种不一样任务源队列中回调函数的读取机制,形成了上述代码中的执行顺序问题。
上图摘自《掘金小册:前端面试之道》
让咱们首先来分析一下上述代码的执行流程:
JavaScript
解析引擎在脚本开头碰到了 console.log
因而打印 script strt
async1()
,async1
执行环境被推入执行栈,解析引擎进入 async1
内部async1
内部调用了 async2
,因而继续进入 async 2
,并将 async 2
执行环境推入执行栈console.log
,因而打印 async2 end
async2
函数执行完成,返回了一个 Promise.resolve(undefined)
,此时,该回调被推入 microtask ,async1
函数中的执行权被让出,等待主线程空闲setTimeout
,等待 0ms 后将其回调推入 macrotask,执行权继续让出Promise
,解析进入注入函数的内部,碰到 console.log
,因而打印 Promise
,再往下,碰到了 resolve
,此时,该回调被推入 microtask ,执行权被让出console.log
,打印完 script end
async2
的 Promise.resolve(undefined)
执行,此时 await 操做符解析该表达式,获得结果 undefined,并将 async1 [Promise] 函数 标志为 resolve 状态,将 await 后面的代码做为回调,继续推入 microtask,等待执行,执行权被让出new Promise
回调,放入主线程执行,打印结果 promise1
和 promise2
Event Loop
去 microtask
里拿 aysnc1
的回调,打印出 async1 end
microtask
队列空,Event Loop
去 macrotask
里拿到 setTimeout
的回调,放入主线程,打印最后的 setTimeout
微任务包括 process.nextTick
,promise
,MutationObserver
,其中 process.nextTick
为 Node 独有。
宏任务包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
。
async
和 await
上述的面试题里,大部分逻辑解释下来都很好懂,除了一处,就是 await
后的 console.log(async1 end)
和 new Promise
resolve
后的回调,到底哪一个先执行?因为浏览器底层的解析引擎实现不一样,对于不一样的浏览器其结果可能不同(最新版的 chrome 浏览器对于 await 的处理变快了,async1 会先于 promise 1 打印)。
可是相比于这个执行顺序,上述题目衍生出的一个更重要的问题,是对于 async/await
的理解。
对于 async/await
的更详细解释,你们能够参照这篇 理解 JavaScript 的 async/await,懒得看的童鞋能够看下面的结论:
async
关键字包装过,就会返回一个 promise
,若是该函数有返回值,那么这个返回值就会做为 then
处理的 response
,若是没有返回值,那么 then
就处理 undefined
await
表达式,只能用在被 async
包装过的函数里,不然会报错await
表达式后接的函数返回值,类型能够为 promise
,或者其余任何的值,await
后的代码在当前执行环境下,会被阻塞至拿到该函数调用后的结果,等拿到结果后,会将 await
后面的代码继续包装成新的 promise
,并将以前拿到的结果做为 response
传入其中,同时让出线程控权async/await
本质上是 Generator
的语法糖关于这个问题,众说纷纭,不少大佬都说是宏任务先于微任务执行,可是代码的运行结果却显示是微任务先执行。 先看看大佬们的解释:
这里不少人会有个误区,认为微任务快于宏任务,实际上是错误的。由于宏任务中包括了
script
,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。 《掘金小册:前端面试之道》
也就是说,Event Loop
抓取回调的逻辑是先执行宏任务,再执行微任务,再执行宏任务。。。以此循环,本质上来讲,当前执行栈里的代码都属于宏任务,因而等待执行栈清空,宏任务执行完成,浏览器回去 microtask
里抓取微任务来执行,除非 microtask
里没有,才会去 macrotask
抓取任务执行。