node 基础
单线程
主线程
- js 负责解释和执行代码的线程只有一个, 称为主线程;
工作线程
- 浏览器或 node 存在其他线程, 处理 http, dom, IO 等, 统称为工作线程;
意义
- 避免多线程同步的问题;
- 降低应用程序内存占用;
- 降低上下文切换开销;
非堵塞 IO
基本原理
- 基于操作系统异步 IO 接口, 后台进行 IO 操作;
- node 线程继续执行其他任务;
- IO 操作完成后, node 通知应用程序, 进行对应操作;
优缺点
- 提高 IO 并发处理能力, 提高服务器性能;
流程
- node 调用操作系统 IO 接口;
- 操作系统接受并挂起 IO 请求, 立刻返回给 node, node 继续执行其他任务;
- 操作系统基于线程池异步处理 IO 操作;
- IO 操作完成后, 操作系统向 node 发送信号, 并保存结果;
- node 接受信号, 读取处理结果, 执行对应回调函数;
事件驱动
事件驱动
- 基于事件监听, 事件循环和任务队列;
- 某事件触发, node 执行对应事件处理程序;
事件循环
简要流程
- node 启动, 创建事件循环和主线程;
- node 将所有的 IO 请求, 定时器请求和异步函数放入任务队列中;
- 主线程开始一次循环;
- 首先执行同步函数;
- 其次检查任务队列是否具有可执行的任务;
- 若任务队列存在已完成任务, 依次取出并放置于主线程执行;
- 若任务为 IO 请求或定时器;
- node 调用系统 IO 异步接口或定时器接口;
- 将该任务设置为挂起状态, 继续执行其他任务;
- 系统 IO 异步操作或定时器超时, 操作系统通知 node 任务完成, node 将其加入任务队列;
- 其余任务调用对应回调函数, 进入轮询等待阶段, 直至任务执行完成;
- 若任务为 IO 请求或定时器;
- 若不存在, 进行下一次循环;

6 个阶段
- 6 个阶段;
- timers 阶段;
- 处理 setTimeout 和 setInterval;
- 检查每一个计时器, 判断是否到达时间, 达到执行回调函数;
- pending callback/IO callback;
- 处理上一阶段少数未被执行的 IO 回调;
- idle prepare: node 内部使用;
- poll: 检索并执行 IO 相关的回调函数;
- 如果其他队列出现回调, 进入 check 阶段;
- 反之再该阶段一直等待;
- check: 执行 setImmediate() 回调;
- close callback: 执行一些 close() 的回调函数(socket, file);
- timers 阶段;
- 每个阶段具有自己的任务队列;
- 每个阶段之间首先清理 process.nextTick() 对应回调, 再清理其他微任务回调;
宏任务和微任务
- 与浏览器一致, 只是作用时机不同;
- 浏览器在每一个宏任务执行前, 都要检查, 执行和保证微任务队列为空;
- node 只在 6 个阶段之间检查, 执行和保证微任务队列为空;
- 相关 API;
- 微任务: process.nextTick();
- 宏任务: setImmediate();
同步任务和异步任务
- 同步任务: 主线程排队执行, 堵塞线程, 常以 sync 结尾;
- 异步任务: 基于事件循环和任务队列, 不堵塞线程;
// 堵塞函数
copyBigFileSync("A.txt", "B.txt");
// 堵塞线程, 所有操作将在函数执行完成后才会执行
console.log("copy finished");
// 非阻塞式函数
console.log("start copying ... ");
copyBigFile("A.txt", "B.txt", () => {
// ...
});
// 未堵塞线程, 后续操作立刻执行
console.log("copy finished");