Node.js
- node 如何充分利用多核 CPU?
- node 是如何实现多个进程监听同一个端口的?为什么不会报“端口被占用”?参考 https://juejin.cn/post/6911456081336074253
- deno 相对 node 有哪些优化?
- node 中的 buffer 是什么? Stream 是什么? 流的种类有哪些?听说过 pull-stream 么?
架构
模块加载机制
- 计算绝对路径
- 加载模块
2.1 优先从缓存中加载
2.2 加载核心模块
2.3 按路径加载模块
2.4 加载第三方模块。会从当前目录的
node_modules
往上查找,直到根目录。后两者会以绝对路径为 key 生成新的缓存
加载模块(2.3/2.4)
得到模块的绝对路径后,就开始加载模块了。首先确定模块的后缀名,不同的文件对应不同的加载方法,以 .js 和 .json 为例
Module._extensions[".js"] = function (module, filename) {
var content = fs.readFileSync(filename, "utf8");
module._compile(stripBOM(content), filename);
};
Module._extensions[".json"] = function (module, filename) {
var content = fs.readFileSync(filename, "utf8");
try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ": " + err.message;
throw err;
}
};
解析 js 模块的关键函数:
Module.prototype._compile = function (content, filename) {
compiledWrapper(content, this.exports, this.require, this);
};
var compiledWrapper = function (content, exports, require, module) {
eval(content);
};
模块的加载其实就是注入 exports、require、module 三个全局变量,然后执行模块的源码,最后输出模块的 exports 值
CommonJS 是如何解决循环引用问题的?
// main.js
const child = require('./child.js')
console.log('执行 main.js: ', child)
module.exports = 'main'
// child.js
const main = require('./main.js')
console.log('执行 child.js: ', main)
module.exports = 'child'
// 执行 main.js , 输出如下:
// 解析:执行到 child.js 时,main.js 还没有执行完,默认导出空对象
执行 child.js: {}
执行 main.js: child
事件循环
事件循环概览如下所示:
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
Node.js 的事件循环是实现非阻塞 I/O 的基础。以上每个阶段都有一个 FIFO 队列来执行回调,当一个事件完成的时候,内核会通知 Node.js 将回调函数添加到队列中。每个阶段最后都会执行当前阶段产生的微任务(包括 nextTick)
timers
进入这个阶段,主线程会检查时间是否满足定时器(setTimeout、setInterval
)触发的条件,如果满足就执行相应的回调函数,否则离开这个阶段
pending callbacks
此阶段执行某些系统操作的回调,例如 TCP 错误
idle、prepare
这个阶段只供 libuv 内部调用
poll
用于处理大部分 I/O 回调和等待 I/O 事件 进入 poll 且不存在到期定时器,此阶段可能会导致主线程阻塞,该阶段会分为以下两种情况:
poll
队列不为空,同步执行队列poll
队列为空,如果执行了setImmediate
,则结束poll
阶段,进入check
阶段;如果没有,事件循环将阻塞并等待回调添加到poll
队列中执行
一旦 poll
队列为空,事件循环将检查到期的定时器,如果有定时器准备好,就回滚到计时器阶段以执行这些计时器的回调;否则会一直停留在这个阶段,等待 I/O 请求返回结果
check
执行 setImmediate
回调
close
该阶段执行关闭请求的回调函数,比如 socket.on('close', callback)
微任务
微任务分类:
process.nextTick
(优先级更高)Promise.then
微任务会在主线之后和事件循环的每个阶段之后立即执行
issues
把 setTimeout 和 setImmediate 放入同一个 I/O 回调函数内,setImmediate 总是被优先调用
fs.readFile(__filename, () => {
setTimeout(() => {
console.log("timeout");
}, 0);
setImmediate(() => {
console.log("immediate");
});
});
原因是 fs.readFile
的回调是在 poll
阶段执行的,该阶段过后会先轮到 check
阶段,即执行 setImmediate
回调,下一轮事件循环才执行 setTimeout
的回调
那为什么在回调函数外这两者的执行顺序不确定呢?
- 执行同步代码(初始化)
- 执行到
setTimeout
,虽然设置的是 0 毫秒触发,但是被 Node.js 强制改为 1 毫秒,将回调加入times
阶段的队列 - 执行到
setImmediate
,将回调加入check
阶段的队列 - 同步代码执行完毕,进入
Event Loop
- 先进入
times
阶段,检查当前时间是否过去了 1 毫秒,如果过了 1 毫秒,满足setTimeout
条件,执行回调,如果没过 1 毫秒,跳过 - 跳过空的阶段,进入
check
阶段,执行setImmediate
回调
异常监控【WIP】
process.on('uncaughtException')
process.on('unhandledRejection')
周边库【WIP】
- pm2
- nodemon
- ts-node