有别于浏览器环境的Node.js Event loop
之前已经谈到过浏览器中的事件循环机制,可以简单概括为执行栈和任务队列共同实现异步队列的机制,但根据运行环境不同,这套机制也会存在差别。那么Node环境和浏览器环境的区别又是什么呢?
浏览器中的事件循环和microtask
在浏览器中的事件循环中可分为macrotask和microtask。整体代码,事件回调,XHR回调,UI渲染等都是marcotask,promise回调,MutationObserver等则是microtask。每一次marcotask执行完成都会清空式执行microtask中的任务,然后进行UI渲染。这个过程会不断重复,浏览器中的事件循环简单概括就是如此。例子如下:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
这段整体代码是一个marcotask,执行完毕后没有microtask,则进入第二个marcotask——一个定时器,在其中打印timer1,然后再清理microtask——打印promise1。执行完毕后进入第三个marcotask——第二个定时器,再执行其中的microtask——打印promise2。
输出如下:
timer1
promise1
timer2
promise2
那么在Node环境执行下试试看
timer1
timer2
promise1
promise2
出现了差别,两者的事件循环机制果然是不同了。
Node中的事件循环和mircotask
在浏览器中事件循环的主任务为marcotask,但在Node中使用了自己设计的libuv作为事件驱动的跨平台抽象层,封装了一些操作系统的底层特性并对外提供统一API,其中对事件循环的主任务实现分为6个阶段,每个阶段完成时都会去清空mircotask。
timers阶段
这个阶段会去检查timer异步任务是否到达了指定下限时间,到达后会尽可能早的执行回调,但需要注意的是执行时间不是确定的。例子如下:
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
谁先执行是不能确定的。
I/O callbacks阶段
这个阶段执行一些系统操作错误的回调,比如TCP连接失败等。在平时开发中较少用到。
Idle,prepare阶段
仅node内部使用,开发时不用关心
poll阶段
这个阶段比较重要,在node里,任何除定时器,close外的异步方法完成时,都会将其callback加入到该阶段并立即执行。该阶段有两个功能:
- 处理poll队列的callback
- 当到达timers的下限时间时处理timers的callback
既然在这一阶段要处理两种callback,就要协调好处理时机,具体流程如下:
- 如果poll队列中存在callback则同步执行直至队列为空。
- 当队列为空时会去检查代码中是否设置了setImmediate。如果设置了将会结束poll阶段进入到check阶段去执行setImmediate回调。
- 如果没有设置setImmediate,会检查代码中的timers,如果有timers达到了下限时间,事件循环会重启并进入timers阶段。
- 如果没有设置timers,事件循环将会阻塞在该阶段等待callback入队。
check阶段
该阶段处理setImmediate的回调。
close callbacks阶段
如果一个socket阶段被突然关掉,close事件就会在这个阶段被触发,否则将通过process.nextTick触发。
process.nextTick()
最后就可以谈nextTick了,这就是node中的mircoTask,无论在以上6个阶段中的哪个阶段,只要出现了nextTick,都会在当前阶段的操作完成后处理。
-- EOF --