Nodejs事件循环
与浏览器的事件循环不同,Node.js 的事件循环构建在 libuv 库之上,专为 I/O 密集型操作优化。
node启动后,先执行完所有的同步代码,再执行队列中所有的process.nextTick
和微任务,最后进入事件循环。
- Timers:执行 setTimeout() 和 setInterval() 的回调。
注意,其中每执行完一个立刻执行队列中所有的process.nextTick
和微任务,再执行下一个Timer回调。 - IO callbacks:处理一些系统级的 I/O 回调,如TCP错误。
- Idle, Prepare:Node内部使用,不常见。
- Poll:检索新的 I/O 事件,执行与 I/O 相关的回调。(新请求、异步文件读取等)
在此期间如果没有其他阶段的任务需要执行,那么将一直处于 poll 阶段,以保证对IO事件的尽快响应。 - Check:执行
setImmediate()
回调。 - Close Callbacks:处理关闭的回调,如
socket.on('close', ...)
。
在每两个阶段之间都会依次执行process.nextTick
和 微任务(Promise回调、await回调)队列。
注意,同一阶段中,process.nextTick
总是优先于其他微任务执行。
定时器
NodeJS中的定时器是由timers
模块实现的,该模块是全局的,因此无需被require
,但其原理与浏览器端不同,是基于Node.js 事件循环构建。
-
setTimeout
、setInterval
、setImmediate
定时器函数返回值在浏览器端是一个定时器id数字,但在Node端是当前定时器的实例对象,同时其中函数的this
指向也指向该定时器实例对象(箭头函数正常指向global)。 - 定时器函数实例额外具有
unref()
、ref()
方法。
const timer = setTimeout(function(){console.log(this)}, 100);//Timeout
setTimeout(()=>console.log(this==global));//true
timer.unref(); // 事件循环在只有这个定时器时退出
timer.ref(); // 恢复默认行为
- 注意:Node中
setTimeout
有最小间隔1ms,因此根据主代码中同步任务执行情况可能导致setTimeout
比setImmediate
先执行(例如耗时不到1ms,直接进入了Timers阶段)。
因此通常将其放到Timers之后的阶段中执行,即可保证setImmediate
先触发。
const fs = require('fs');
const path = require('path');
// 在IO结束的callback执行 也就是 poll 阶段会执行该 fs.readFile callback
// IO回调中存在 setImmediate 那么EventLoop的下一个阶段一定会进入check阶段
// 进而一定会优先执行 setImmediate 的回调
fs.readFile(path.resolve(__dirname, 'package.json'), (err) => {
if (err) {
console.log(err, 'err');
}
setTimeout(() => {
console.log('timer');
});
setImmediate(() => {
console.log('immediate');
});
});
-
setImmediate
可用在当前I/O 操作完成后立即执行某任务,或用于拆分复杂任务,避免阻塞事件循环。
比起setTimeout(fn,0)
开销更小,比起process.nextTick
不容易饿死IO(阻塞事件循环)。
function heavyComputation(data) {
if (data.length === 0) return;
// 处理一部分
processChunk(data.splice(0, 100));
// 延迟下一轮
setImmediate(() => heavyComputation(data));
}
性能
内存
NodeJs的垃圾回收和浏览器端差不多,额外有一些指令:
-
node --expose-gc xxx.js
允许在代码中手动调用global.gc()
触发垃圾回收。 -
node --trace_gc xxx.js
node --trace_gc_nvp xxx.js
打印垃圾回收的情况 - 生成内存快照
可用于查看内存中的对象数量、大小,并进行定位。
const { writeHeapSnapshot } = require('node:v8');//此处"node:"是NodeJs12+的新增语法,用于防止引用的模块重名
v8.writeHeapSnapshot()
- 调整新/旧堆空间大小:
node --max-semi-space-size=64 app.js
node --max-old-space-size=4096 app.js
- 调整垃圾收集频率(ms)
node --gc-interval=100 app.js
-
process.memoryUsage
查看当前进程内存使用情况
process.memoryUsage()
{
rss: 35438592,
heapTotal: 6799360,
heapUsed: 4892976,
external: 939130,
arrayBuffers: 11170
}
CPU
- 使用V8内置分析器
使用--prof
启动
node --prof xxx.js
将得到的log文件转化成txt格式:
node --prof-process isolate-0xnnnnnnnnnnnn-v8.log > processed.txt
- 使用火焰图
调试
- 使用
--inspect
指令进行调试
通过node --inspect xxx.js
指令启动NodeJs后,进入Chrome浏览器chrome://inspect
页面,在Remote Target
部分即可使用Chrome DevTools调试NodeJs应用,可以在其中打断点、使用Console、Network、Performance、Memory等。 - 在VSCode中进行调试