nodejs 事件循环机制与浏览器的区别

区别大了

文档

尽管大多数情况它们表现相同,但浏览器的事件循环机制是基于 html5 标准的,nodejs是基于 libuv 的,所以这二者肯定是不同的。

nodejs
html

浏览器的事件循环

浏览器的事件循环相比 node 而言,简单多了,他将任务分为宏任务和微任务

  • macro-task(宏任务) 比如: setTimeout、setInterval、script(整体代码)、 I/O 操作、UI 渲染等
  • micro-task (微任务)比如: Promise、 MutationObserver 等。

从 macro-task队列中(task queue)取一个宏任务执行, 执行完后, 取出所有的 micro-task 执行。macro-task 出栈是一个个出的,micro-task 是一组一组出的

node 事件循环

node 启动后,就会按以下顺序执行,直到有异步事件触发,再次进入新的循环。(以下其实就是libuv的执行阶段)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check(setImmediate)│
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

Timers(计时器):

这里执行 setTimeout 和 setInterval 定时器的回调,如果到这一步,参数时间到了,会直接执行。并且它们的参数是有下限时间的,[1, 2147483647],就算你设置了 0,也会修正为 1.

I/O回调:

一般处理由系统或者网络错误抛出的异常回调函数,比如 TCP Error

idle, prepare:

nodejs 内部函数调用。不需要讨论
poll 维护着一个队列,这个队列存储着除了 Timer\setImmediate\close 之外的异步回调函数,当异步函数完成时,poll 负责执行 callback。
以下是poll(轮询阶段,很关键)的流程,我用伪代码表现流程:

1
2
3
4
5
6
7
8
9
10
11
if(poll队列不为空) {
事件循环先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。
} else {
if(代码已经被setImmediate()设定了回调) {
直接结束poll阶段进入check阶段来执行check队列里的回调
} else if(如果有被设定的timers) {
如果有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列。
} else {
事件循环会阻塞poll阶段等待回调被加入poll队列。
}
}

这句话多看几遍,事件循环会阻塞在poll阶段等待回调被加入poll队列。

close callbacks

执行各种 close 回调比如

1
2
3
p.on('close', function (code) {
console.log('子进程已退出,退出码 '+code);
});

实例

1
2
3
4
5
6
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})

在node 中,这两者的执行顺序是随机的(多运行几次)。首先,上面讲到,setTimeout 的参数会被修正为 1。当 setTimeout 先打印时,我们模拟一遍流程。

  • 当前进程性能突然变低,等 timeout 执行的时候,时间已经大于等于1了,直接执行回调。timer…… -> poll -> setImmediate

如果执行那一刻进程性能高一点,从 Timers(不执行) -> poll,此时,poll 队列为空,有 setImmediate,进入 check 阶段 -> callbacks -> Timers -> …… -> poll -> 执行 Timer

process 和 Promise

process.nextTick()不在以上流程中,它会在主逻辑的末尾任务队列调用之前调用。也在 当前事件循环的最后,下次事件循环之前调用。

怎么理解这句话呢,改造下上面的 demo

1
2
3
4
5
6
7
8
9
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
process.nextTick(() => {
console.log('nextTick');
})

你会发现,再也不随机了。结果是

1
2
3
nextTick
setTimeout
setImmediate

这个时候,运行顺序就是: 主流程 -> nextTick -> Timer -> setImmediate
process.nextTick 让事件循环延迟了一小会儿,所以 timeout 的时间总是大于 1 了。
Promise 和 process.nextTick 的执行阶段是一样的,但它比后者优先级低。

验证下你真的懂了吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
console.log(1);
setTimeout(() => console.log('setTimeout=> 1'),0);
process.nextTick(() => console.log('nextTick=> 1'));
console.log(2);
setTimeout(() => console.log('setTimeout=> 2'),0);
process.nextTick(() => {
console.log('nextTick=> 2');
for (let i = 0; i < 10000222200; i++) {}
});
console.log(3);
process.nextTick(() => console.log('nextTick=> 3'));
setTimeout(() => console.log('setTimeout=> 3'),0);
console.log(4);
setTimeout(() => console.log('setTimeout=> 4'),0);
process.nextTick(() => console.log('nextTick=> 4'));
console.log(5);

for (let i = 0; i < 10000222200; i++) {}

总结

这二者表现大多数情况是相同的,但其原理,区别大了。

参考

nodejs 事件循环
html5 事件循环
node 事件循环机制