1. Node事件循环简介
在理解Node事件循环中的微任务队列之前,我们首先需要了解Node事件循环本身。Node事件循环是Node.js运行在单线程上的核心。事件循环会不断地将消息队列中的事件放到回调队列中,Node.js执行这些回调并且返回结果,这个过程就是Node事件循环。
下面是Node事件循环的基本流程:
while (eventLoop_not_empty) {
eventLoop();
}
而事件循环的过程又可分为以下几个阶段:
轮询阶段(poll)
检测阶段(check)
关闭事件回调阶段(close callbacks)
定时器阶段(timers)
I/O事件回调阶段(I/O callbacks)
闲置阶段(idle)
唤醒(或很少见的检测)线程阶段(prepare)
进入poll阶段前,执行一些额外的JS(或C++)的代码,这些可通过process.nextTick()或setImmediate()来添加。具体代码可以在指定的阶段之间(如在关闭事件回调阶段和定时器阶段之间)或离开事件循环之前执行。它们是像process.nextTick()和setImmediate()这样的API的运行机制的一部分
2. 微任务和宏任务
在Node事件循环中,任务可以分为微任务和宏任务。微任务指的是所有回调函数汇总成的一个任务队列,而宏任务则是指一个个独立的任务。为了区分微任务和宏任务之间的关系,我们需要深入了解它们各自是如何执行的。
2.1 微任务
微任务指的是所有回调函数汇总成的一个任务队列,这些回调函数都是在当前事件循环中由Promise或process.nextTick()添加到队列中的。
在Node事件循环中,当异步函数完成时,它创建的Promise的then()方法中的回调将被执行。这些回调函数是微任务队列中的一员。微任务队列的执行时机是在当前事件循环中自上而下地运行到空闲状态时进行的,且在Node.js本身完成的操作之前执行。因此,如果有多个then()方法添加了回调,那么它们将按照添加的顺序依次执行。这些回调函数在执行时是FIFO(先进先出)排序的。
2.2 宏任务
相反的,宏任务是单独的任务,既不在Promise.then()回调队列中,也不在process.nextTick()回调队列中。在Node事件循环中,一些常见的宏任务类型包括:
定时器:setTimeout()、setInterval()
I/O操作:fs.readFile()、http.get()
setTimeout()和setInterval()的setTimeout()
setImmediate()
process.nextTick()
宏任务的执行时机则类似于一个消息队列,每次事件循环只处理一个宏任务。当事件循环处理完第一个宏任务后,它将去检查并执行微任务队列中的回调函数,执行完后再去处理下一个宏任务。这个过程如此循环下去,直至事件循环中的消息队列为空为止。
3. 微任务队列优化
Node事件循环阶段顺序的基本流程是轮询、定时器、I/O事件回调、关闭事件回调,而其中轮询阶段是最耗时的一步。对于微任务,它们将在轮询后立即执行,因此,我们可以优化微任务队列的执行顺序。为了更好地了解这个过程,让我们考虑以下代码:
Promise.resolve().then(() => {
console.log('promise 1')
}).then(() => {
console.log('promise 2')
})
console.log('script end')
正确的输出顺序应该是:
script end
promise 1
promise 2
这时因为Promise处理完后会将then()回调加入到微任务队列中,而当轮询完成后,任务调度器会立即执行微任务队列中的回调函数,所以最先输出的是"promise 1"。
假设我们想要优化微任务队列的执行顺序,将第二个回调挪到了宏任务队列中,代码如下:
Promise.resolve().then(() => {
console.log('promise 1')
setTimeout(() => {
console.log('setTimeout 1')
}, 0)
}).then(() => {
console.log('promise 2')
})
console.log('script end')
再次验证输出顺序,正确的应该是:
script end
promise 1
promise 2
setTimeout 1
在此例中,我们将第二个Promise回调添加到第一个微任务回调中,并在其中创建一个延迟为0毫秒的setTimeout(1)延迟任务。这个setTimeout()不但被添加到了当前事件循环的一个新的宏任务列表中,而且在所有微任务完成后才会被执行。因此在输出的顺序如上所示。
这个小技巧可以让我们平衡当前和下一个事件循环周期之间的任务处理。当我们需要长时间的异步处理时,将第二个Promise回调添加到当前microtask中是最优选择,因为这可以尽快完成微任务队列中的任务。而如果任务的处理时间很短,那么我们可以将它添加到宏任务列队中,以便在下一个时间循环中继续执行。
4. 微任务队列总结
在Node事件循环中,当映射到事件循环的回调完成时,它们将添加到任务队列中,并可以被视为宏任务或微任务。微任务队列可以确保在下一个事件循环周期之前立即执行,并且在事件循环中的其他操作之前。理解微任务队列可以帮助开发者更好地控制JS中的异步函数,从而设计更可靠的应用程序。