JavaScript 是单线程的,因为单线程会形成I/O阻塞,好比发送请求时未响应就可能形成页面停滞,为了解决这个问题,浏览器开始支持异步JS,异步JS就是把一些异步任务(ajax、定时器)等放到任务队列中,而后经过事件循环不断读取、触发任务队列中的异步代码,这种机制就叫作事件循环Event Loop。node
Event Loop的核心代码是采用c++写的(属于NodeJs的范畴),本质上来讲Event Loop就是采用轮询的方式不断读取、执行事件,今天咱们要讨论的就是事件循环中的细节。c++
阶段Event Loop内部分为如下阶段面试
┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<─────┤ connections, │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘复制代码
上面的每个阶段都有一个队列(先进先出),里面存放回调函数。每当Event Loop到达一个阶段,通常来讲都会执行队列中的某些函数(也有可能不操做)ajax
一个 Node.js 程序结束时,Node.js 会检查 event loop 是否在等待异步 I/O 操做结束,是否在等待计时器触发,若是没有,就会关掉 event loop。promise
这个阶段颇有多是Event Loop开始的第一个阶段,主要存放setTimeout或者setInterval等宏任务浏览器
这个阶段主要用来获取新的I/O事件,当 event loop 进入 poll 阶段,发现 poll 队列为空,event loop 检查了一下最近的计时器,大概还有 100 毫秒时间,因而 event loop 决定这段时间就停在 poll 阶段,当定时器任务快开始的时候,Event Loop会绕过poll阶段进入check阶段异步
这个阶段有一个API,面试的时候常常用的到,属于nodeJS的setImmediate,它一样属于宏任务,可是相对于定时器来讲,它的特色是要求更快执行socket
setImmediate和setTimeoutsetImmediate 和 setTimeout 很类似,可是其回调函数的调用时机却不同。async
从所属的阶段队列来看,setImmediate属于check阶段,setTimeout属于timers阶段,那么二者之间到底谁先执行呢?ide
先看一段代码
setTimeout(()=>{console.log('timeout')},0) setImmediate(()=>{console.log('immediate')},0)复制代码
通常来讲,都会优先执行setTmmediate,可是上面的代码实际执行顺序是这样的为何会有时先执行timeout,有时先执行immediate呢,这要从顺序看起,若是上面的代码setTimeout的时间设定为1000ms,那你们必定不会感到困惑,因为immediate存在于check阶段,当时间设定为1000ms时,Event Loop处于poll阶段,毕竟要等到1000ms才执行timers队列中的函数,因此Loop打算休息一下。
而后呢好像时间差很少了,Loop发现check阶段有个immediate函数,因而跑过去执行一下,执行完了就再跑到timers阶段执行。
而上面产生困惑的最大缘由是定时器设置时间为0,这就要看Event Loop开始时,所处的阶段。
若是Event Loop此时在timers阶段,队列中尚未定时器任务,又或者定时器任务还没到时间,那么必然会跳过此阶段,优先执行immediate任务。
若是此时有任务,并且时间到了,那么必然会先执行setTimeout,这也是上述代码产生困惑的缘由。
下面咱们对它进行改写
setTimeout(() => { setTimeout(() => {console.log("timeout"); }, 0); setImmediate(() => {console.log("immediate"); }); }, 1000);复制代码
上面的代码,1秒后,执行箭头函数,此时Event Loop并不在timers阶段,因为顺序是不可变的,因此老是会优先执行immediate
你可能发现 process.nextTick() 这个重要的异步 API 没有出如今任何一个阶段里,那是由于从技术上来说 process.nextTick() 并非 event loop 的一部分。实际上,无论 event loop 当前处于哪一个阶段,nextTick 队列都是在当前阶段后就被执行了。
setTimeout(() => { setTimeout(() => {console.log("timeout"); }, 0); setImmediate(() => {console.log("immediate"); }); process.nextTick(()=>{ console.log('nexTick') }) }, 1000);复制代码
上面的代码执行顺序是这样的
nextTick是在当前阶段立刻执行,因为上面的代码执行后Loop处于poll阶段,因此会优先执行nextTick
为了更好得实验,咱们再改一下代码
setTimeout(() => { setTimeout(() => {console.log("timeout"); process.nextTick(() => { console.log("nexTick2"); }); }, 0); setImmediate(() => {console.log("immediate"); }); process.nextTick(() => {console.log("nexTick"); }); }, 1000);复制代码
下面是结果,能够发现nextTick是在当前阶段立刻执行的
nexTick immediate timeout nexTick2复制代码process.nextTick() 和 setImmediate()
这两个函数功能很像,并且名字也很使人疑惑。
process.nextTick() 的回调会在当前 event loop 阶段「当即」执行。 setImmediate() 的回调会在后续的 event loop 周期(tick)执行。
两者的名字应该互换才对。process.nextTick() 比 setImmediate() 更 immediate(当即)一些。
这是一个历史遗留问题,并且为了保证向后兼容性,也不太可能获得改善。因此就算这两个名字听起来让人很疑惑,也不会在将来有任何变化。
咱们推荐开发者在任何状况下都使用 setImmediate(),由于它的兼容性更好,并且它更容易理解。
宏任务和微任务异步任务中分宏任务和微任务,微任务老是比宏任务先执行
setTimeout(()=> console.log(4))//宏任务new Promise(resolve => { resolve()//同步任务 console.log(1) //同步任务}).then(()=> { console.log(3) //微任务})console.log(2) //同步任务复制代码
改形成await
setTimeout(_ => console.log(4)) //宏任务async function main() { console.log(1) //同步任务 await Promise.resolve() //同步任务 至关于 resolve() console.log(3) //至关于promise.then //微任务} main()console.log(2) //同步任务复制代码