心智模型

Node 是单线程的——同一时刻只跑一段 JS。异步靠事件循环调度。

┌─────────────────────────────────┐
│           Call Stack            │ ← 现在跑的代码
└─────────────────────────────────┘
            ↑
            │ 取下一个任务
┌───────────┴─────────────────────┐
│         Event Loop              │ ← 调度器
└─────────────────────────────────┘
        ↑           ↑
        │           │
  ┌─────┴───┐  ┌────┴──────┐
  │ Tasks   │  │ Micro     │ ← 两种队列
  │ Queue   │  │ tasks     │
  └─────────┘  └───────────┘

事件循环的 6 个阶段

┌─────────────────────────────┐
│   timers                    │ setTimeout / setInterval 到期
├─────────────────────────────┤
│   pending callbacks         │ 系统操作(如 TCP 错误)
├─────────────────────────────┤
│   idle, prepare             │ 内部
├─────────────────────────────┤
│   poll                       │ 拉新 I/O 事件,执行回调
├─────────────────────────────┤
│   check                      │ setImmediate 回调
├─────────────────────────────┤
│   close callbacks            │ socket.on('close') 等
└─────────────────────────────┘
        每个阶段之间清空 microtask 队列

循环按顺序经过这 6 个阶段——叫一个 "tick"。

任务 vs 微任务

Microtask

  • Promise.then / catch / finally
  • queueMicrotask(fn)
  • process.nextTick(fn) ← Node 独有,比微任务还优先

Macrotask / 任务:

  • setTimeout / setInterval
  • setImmediate
  • I/O 回调(fs / net 等)

执行顺序

console.log('1');                          // 同步

setTimeout(() => console.log('2'), 0);     // 宏任务

queueMicrotask(() => console.log('3'));    // 微任务

Promise.resolve().then(() => console.log('4'));   // 微任务

process.nextTick(() => console.log('5'));  // nextTick(Node 优先)

console.log('6');                          // 同步

输出:

1
6
5         ← nextTick 最先(在所有微任务前)
3         ← microtask
4         ← microtask
2         ← 等当前同步代码 + 微任务全跑完,才轮到 setTimeout

记忆:

同步代码 → process.nextTick → microtask(Promise / queueMicrotask) → 进入循环阶段

setImmediate vs setTimeout(fn, 0)

setImmediate(() => console.log('immediate'));
setTimeout(() => console.log('timeout'), 0);

输出顺序不确定(看启动时机)。

但在 I/O 回调里

fs.readFile('a.txt', () => {
    setImmediate(() => console.log('immediate'));
    setTimeout(() => console.log('timeout'), 0);
});

immediate 永远先——因为 I/O 阶段后紧接 check 阶段。

process.nextTick:双刃剑

process.nextTick(() => {
    // 在当前阶段结束、microtask 之前执行
});

特点:

  • 比 microtask 还早
  • 用于"在当前操作完成后立即跑"

递归调用会饿死事件循环

function bad() {
    process.nextTick(bad);   // I/O 永远轮不到
}
bad();                        // 系统假死

实战很少手动 nextTick——库内部用得多。

queueMicrotask vs Promise.then

queueMicrotask(() => console.log('a'));
Promise.resolve().then(() => console.log('b'));

两者都是微任务,顺序是注册顺序。queueMicrotask 更轻量——不创建 Promise 对象。

阻塞事件循环 = 性能杀手

// ❌ CPU 密集同步代码 → 阻塞循环
function fibonacci(n) {
    if (n < 2) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
}

server.on('request', () => {
    fibonacci(40);    // 阻塞 1+ 秒,期间所有请求挂起
});

解决:

  • 把 CPU 密集任务拆成小块(每几 ms 让出循环 setImmediate
  • worker_threads 真多线程(见 24-worker-threads)
  • child_process 起子进程

看事件循环延迟

import { monitorEventLoopDelay } from 'perf_hooks';

const h = monitorEventLoopDelay();
h.enable();

setInterval(() => {
    console.log('Mean delay ns:', h.mean);    // 越高越糟
    h.reset();
}, 1000);

平均延迟 > 几十毫秒 → 事件循环卡了。

  • 阻塞循环 = 全站慢:任何长同步代码都会拖累所有连接
  • process.nextTick 递归 / 大量调用饿死循环
  • 现代代码不需要手动 setImmediate / nextTick——理解概念知道顺序就行
  • setTimeout(fn, 0) 最小延迟实际是 4ms(在某些情况下)

至此完成异步基础 4 篇。下一篇起进入核心 API:fs / path / streams / events / http 等。