对于事件循环机制,我在初学JavaScript的时候了解了宏队列微队列便是事件循环的全部,但最近看了《深入浅出nodeJS》才发现我真是young and naive了。
前置知识——调用栈
JS 中会存在一个调用栈,它会负责跟踪所有待执行的操作。每当一个函数执行完成时,它就会从栈的顶部弹出。比如下面这段代码:
const bar = () => console.log('bar')const baz = () => console.log('baz')const foo = () => {console.log('foo')bar()baz()}foo()
浏览器中的事件循环
NodeJS中的事件循环
nodejs的事件循环机制是由底层libuv提供的异步IO方案。事件循环整体架构图如下图所示:
图中有8个矩形框,其中6个构成一个环,先从构成环的6个说起。
6大阶段(宏任务)
Timer
这是事件循环的开始阶段。此阶段的队列中包含setTimeout()和setInterval()的回调函数。具体而言,在代码执行过程中产生的延时函数回调,在进入队列中会按照延时的大小从小到大排序,当事件循环到本阶段时,会将到时间了的回调函数出队并执行。没有到时间的继续呆在队列中。
Pending I/O callbacks(不作为关注重点)
Idle, Prepare phase(不作为关注重点)
在这个阶段,事件循环什么都不做。它处于空闲状态,准备进入下一阶段。
poll
在这个阶段,事件循环留意新的异步 I/O 回调。除了 setTimeout、setInterval、setImmediate 和关闭回调之外,几乎所有的回调都被执行。poll阶段的流程如下所示:
Check
check 阶段会检测setImmediate()回调函数并在这个阶段进行执行。
close callbacks(不作为关注重点)
这个阶段执行一些列关闭的回调函数socket.on('close', callbacks)
Promise()的回调(微任务)
类似的,事件循环还存在一个微任务队列,微任务队列执行时机是:当前调用栈产生了微任务,微任务进入微任务队列,当调用栈清空后,且Process.nextTick()队列为空,则立马清空微任务队列。
process.nextTick()
Process.nextTick()的执行时机即是在同步任务执行完毕后,立即将Process.nextTick()队列中的回调函数依次推入调用栈中进行执行。
换句话说,所谓的Process.nextTick()简单来说就是表示当前调用栈清空后立即执行的逻辑,其实你完全可以将它理解成一个 micro (虽然官方并不将它认为是 EventLoop 中的一部分)。
案例学习
案例1 setTimeout 和 nextTick
function tick() {console.log('tick');}function timer() {console.log('timer');}setTimeout(() => {timer();}, 0);process.nextTick(() => {tick();});// 输出: tick timer
上述代码的调用过程其实非常简单,当代码依次执行时遇到 process.nextTick 和 timer 时会分别将他们推入对应的 Queue 中。
当调用栈中的代码执行完毕时,会先检查 nextTick 中存在对应的 tick 函数,那么会拿出 tick 进入调用栈中进行执行。
当 nextTick 中的任务清空时,会进入所谓的 timers 阶段依次将Timer队列中任务推入调用栈中。直至Timer队列可执行的回调清空。
案例2 setTimeout 和 setImmediate
function timer() {console.log('timer');}function immediate() {console.log('immediate');}setTimeout(() => {timer();}, 0);setImmediate(() => {immediate();});/**输出:timer 或者 immediateimmediate timer*/
分析执行过程:
- 同步代码执行时,这段代码会在 timer 以及 check 阶段的队列中分别推入对应的 timer 函数和 immediate 函数。此时代码跑完即调用栈为空。
- 检查
Process.nextTick()回调队列,发现是空。 - 进入事件循环
- 进入Timers阶段,发现回调函数有可能到时间了。为什么说有可能呢?因为ndoejs中
setTimout(callbacks, delay)delay在0~2147483647 之外时,会将delay设置为1。因此,不确定进入Timer阶段回调函数是否到时间。 - 进入check阶段,发现队列不为空,执行其回调函数。
案例3 如何保证 setImmediate 一定早于 setTimeout 执行回调函数
```javascript const fs = require(‘fs’); const path = require(‘path’);
fs.readFile(‘./a.js’, (err) => { if (err) { console.log(err, ‘err’); }
setTimeout(() => { console.log(‘timer’); });
setImmediate(() => { console.log(‘immediate’); }); }); // 输出: // immediate // timer
分析:<br />在 poll 阶段会执行该 fs.readFile 的回调函数,回调中存在 setImmediate 那么EventLoop的下一个阶段一定会进入check阶段 。而setTimeout就得到下一轮事件循环才能执行了。<a name="KIyhI"></a>#### 案例4 理解宏任务 微任务 执行过程```javascriptsetImmediate(() => {console.log('immediate1 开始')Promise.resolve().then(() => console.log('immediate' + 1, '微任务执行'));Promise.resolve().then(() => console.log('immediate' + 2, '微任务执行'));console.log('immediate1 结束');});setImmediate(() => {console.log('immediate2 开始');Promise.resolve().then(() => console.log('immediate' + 3, '微任务执行'));Promise.resolve().then(() => console.log('immediate' + 4, '微任务执行'));console.log('immediate2 结束');});/**实际输出:immediate1 开始immediate1 结束immediate1 微任务执行immediate2 微任务执行immediate2 开始immediate2 结束immediate3 微任务执行immediate4 微任务执行原本预想的输出:immediate1 开始immediate1 结束immediate2 开始immediate2 结束immediate1 微任务执行immediate2 微任务执行immediate3 微任务执行immediate4 微任务执行*/
正确的步骤:
- 执行同步代码,发现两个setTimeout,将其回调函数放到Timer阶段中。调用栈清空
- 检查
Process.nextTick()回调队列,发现是空。 - 进入事件循环Timer阶段队列有两个任务。第一个回调函数出队进入调用栈中执行。
- 执行打印
immediate1 开始遇到两个promise回调函数,加入到微任务队列中。之后打印immediate1 结束当前调用栈清空。 - 微任务队列不为空,有两个任务。立马依次送入调用栈。依次打印
immediate1 微任务执行和immediate2 微任务执行。调用栈再次为空。 - TImer阶段队列第二个回调函数进入调用栈执行。
- 后续步骤同上。
参考链接
关于Node.js EventLoop的poll阶段该如何理解?
Node.js event loop workflow & lifecycle in low level
