node

node事件循环

Node.js采用V8作为js的解析引擎,而I/O处理方面使用了自己设计的libuv,libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现

  • V8引擎解析JavaScript脚本

  • 解析后的代码,调用Node API

  • libuv库负责Node API的执行。将不同的任务分配给不同的线程,形成一个EventLoop,以异步的方式将任务的执行结果返回给V8引擎

  • V8引擎再将结果返回给用户

event loop 机制

node 事件机制分六个阶段

timers -> pending callbacks -> idle, prepare -> poll -> check -> close callbacks

定时器:执行 setTimeout 和 setInterval 回调函数

待定回调:执行一些系统操作回调,比如 TCP 错误,名字会让人误解为执行 I/O 回调处理程序

idle, prepare:仅系统内部使用

轮询:检索新的 I/O 事件;执行与 I/O 相关的回调,其余情况将在适当的时候在此 阻塞

检测:setImmediate() 回调函数在这里执行

关闭回调函数:一些关闭的回调函数,如:socket.on(‘close’, …)

一些读写,以及用户的交互操作,都在 poll 轮询阶段处理

初始化

当 Node.js 启动后,它会初始化事件循环,处理已提供的输入脚本,它可能会调用一些异步的 API、调度定时器,或者调用 process.nextTick(),然后开始处理事件循环

  • 执行输入代码

  • 执行 process.nextTick 回调

  • 执行 microtasks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
setTimeout(() => {
console.log('macro');
}, 0);

var micro = new Promise((resolve) => {
resolve('micro')
})
micro.then(res => {
console.log(res)
})
process.nextTick(() => {
console.log('nextTick')
})

// nextTick
// micro
// macro

timer

timers 阶段会执行 setTimeout 和 setInterval 回调,并且是由 poll 阶段控制的。在 Node 中定时器指定的时间也不是准确时间,只能是尽快执行

  • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行

  • 检查是否有 process.nextTick 任务,如果有,全部执行

  • 检查是否有microtask,如果有,全部执行

  • 退出该阶段

pending callbacks

  • 检查是否有 pending 的 I/O 回调。如果有,执行回调

  • 检查是否有 process.nextTick 任务,如果有,全部执行

  • 检查是否有microtask,如果有,全部执行

  • 退出该阶段

poll

两个重要功能

  • 处理 poll 队列的事件

  • 当有已超时的 timer,执行它的回调函数

进入该阶段,且轮询队列不是空

  • 事件循环将同步执行 poll 队列里的回调,直到队列为空或执行的回调达到系统上限

  • 如果没有其他阶段的事要处理,事件循环将会一直阻塞在这个阶段,等待新的 I/O 事件加入 poll 队列中

进入该阶段,且轮询队列是空

 1. 如果 check 队列已经被 setImmediate 设定了回调, 事件循环将结束 poll 阶段往下进入 check 阶段来执行 check 队列里面的回调

  • 检查是否有 process.nextTick 回调,如果有,全部执行

  • 检查是否有 microtaks,如果有,全部执行

 2. 如果 setImmediate 没有回调,则事件循环将等待在 poll 阶段,等待新的回调添加到队列中,然后立即执行

  • 检查是否有 process.nextTick 回调,如果有,全部执行

  • 检查是否有 microtaks,如果有,全部执行

 3. 一旦轮询队列为空,发现有一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const fs = require('fs')

const readStartTime = Date.now()

setTimeout(() => {
console.log('延时8毫秒')
}, 8)
setTimeout(() => {
console.log('延时200毫秒', `实际执行用时${Date.now() - readStartTime}`)
}, 200)

function someAsyncOperation(callback) {
fs.readFile('./file.js', callback);
}

someAsyncOperation(() => {
const timestart = Date.now()
while (Date.now() - timestart < 500) {}
console.log(`读取文件和执行回调耗时${Date.now() - readStartTime}`)
})

setTimeout(() => {
console.log('延时0毫秒')
})
setImmediate(() => {
console.log('setImmediate')
})

// setImmediate 和 延时0毫秒的 setTimout 随机1,2位
// 延时0毫秒
// 延时8毫秒
// 读取文件和执行回调耗时548
// 延时200毫秒 实际执行用时549

为什么 setTimeout 和 setImmediate 打印先后不一?

  • setTimeout 在 timers 阶段执行,而 setImmediate 在 check 阶段执行,且 setTimeout 延时是0,理论setTimeout会早于setImmediate完成

  • 但实际上,Node 做不到0毫秒,最少也需要1毫秒,setTimeout(f, 0)等同于setTimeout(f, 1)

  • 实际执行进入事件循环以后,有可能到了1毫秒,也可能没到,取决于运行状况。如果没到,说明 timers 阶段没有到期的回调可以执行,进入下一个阶段

但是,下面的代码一定是先输出2,再输出1

代码会先进入 poll 阶段,然后是 check 阶段,最后才是 timers 阶段

1
2
3
4
5
const fs = require('fs');
fs.readFile('test.js', () => {
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
});

为什么200毫秒的timer回调会到549之后才执行?

  • node 初始化后,进入timer阶段,此时可能过了 1ms 也可能没有,如果过了,则执行回调用打印 延时0毫秒,此时队列已经没有可执行的回调。结束并进入下一个阶段

  • 进入 pending callbacks 阶段,发现队列为空,结束并进入下一个阶段

  • 进入内部阶段无(idle,prepare),发现队列为空,结束并进入下一个阶段

  • 进入 poll 循环阶段,此时它有一个空队列(fs.readFile 尚未完成,一般要100ms左右),而空队列的时候有两种情况,有 setImmediate 调度则进入 check 阶段,执行 setImmediate, 没有 setImmediate 则事件循环将等待(阻塞在此)回调(I/O回调)被添加到队列中,然后立即执行

  • 第一轮大循环存在 setImmediate 且未执行,因此结束poll 阶段,进入check 执行 setImmediate,执行完后,check队列已经清空,结束并进入下一个阶段

  • 进入关闭阶段,发现队列为空,结束

  • 检查是否有活跃度的 handles(定时器、IO等事件句柄),发现仍存在未执行的内容,再次进入timer

  • 而此时 8 ms的定时器已经到达时间限制(执行上面的代码花费时间不足200ms,所以 200ms 还没有到),将该回调拿出并执行

  • 执行完成后不再有可执行回调,再次结束进入下一个阶段

  • 进入 pending callbacks 阶段,发现队列为空,结束并进入下一个阶段

  • 进入内部阶段无(idle,prepare),发现队列为空,结束并进入下一个阶段

  • 再次到达 poll 阶段时,此时 readFile 可能完成,亦可能未完成,如果完成了,则回调进入poll队列,立即同步至执行,如果未完成,队列依然为空。检测 setImmediate 是否有调度

  • 上一个循环 setImmediate 已经执行完, 所以没有 setImmediate 调度,则事件循环将在此阶段等待回调(readFile的回调)被添加到队列中,然后立即执行(执行花费500ms,此时setTimeout 已经到时间了,但是阻塞在这里)

  • readFile的回调执行结束,poll 轮询清空,发现此时已经有到期的定时器,然后绕回到timer,执行 setTimeout 回调

check阶段

setImmediate()的回调会被加入check队列中

  • 如果有immediate回调,则执行所有immediate回调

  • 检查是否有 process.nextTick 回调,如果有,全部执行

  • 检查是否有 microtaks,如果有,全部执行

  • 退出 check 阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
setImmediate(() => {
console.log('setImmediate 一次')
})

setImmediate(() => {
console.log('setImmediate 二次')
})

process.nextTick(() => {
console.log('nextTick')
})

const P = new Promise((resolve) => {
resolve('micro')
})
P.then(res => {
console.log(res)
})
// nextTick
// micro
// setImmediate 一次
// setImmediate 二次

检查

检查是否有活跃的 handles(定时器、IO等事件句柄)

  • 如果有,继续下一轮循环

  • 如果没有,结束事件循环,退出程序

参考

node机制

返回
顶部