{T}

Node.js 微任务与宏任务

Node.js 特有的任务队列

Node.js 在标准的微任务和宏任务之外,还提供了独有的 process.nextTick 队列。

任务队列层次结构

code
┌─────────────────────────────────────────────────────┐
│                    执行顺序(从上到下)               │
├─────────────────────────────────────────────────────┤
│  1. 同步代码                                         │
├─────────────────────────────────────────────────────┤
│  2. process.nextTick 队列(Node.js 特有)            │
├─────────────────────────────────────────────────────┤
│  3. Promise 微任务队列                               │
│     - Promise.then/catch/finally                    │
│     - queueMicrotask                                │
│     - async 函数内的 await                          │
├─────────────────────────────────────────────────────┤
│  4. 宏任务队列                                       │
│     - setImmediate                                  │
│     - setTimeout / setInterval                      │
│     - I/O 回调                                      │
│     - setImmediate(check 阶段)                    │
└─────────────────────────────────────────────────────┘

process.nextTick 队列

基本用法

javascript
// process.nextTick 是 Node.js 特有的微任务 API
// 它的优先级比 Promise 微任务更高

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

Promise.resolve().then(() => {
  console.log('Promise 1');
});

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

console.log('同步代码');

// 输出顺序:
// 同步代码
// nextTick 1
// nextTick 2
// Promise 1

process.nextTick 的特点

javascript
// 1. 最高优先级:在所有微任务之前执行
process.nextTick(() => console.log('1. nextTick'));
Promise.resolve().then(() => console.log('2. Promise'));

// 2. 在当前操作完成后立即执行
function processData(data, callback) {
  // 数据处理是同步的
  const result = data.toUpperCase();
  
  // 使用 nextTick 确保回调异步执行
  process.nextTick(() => callback(result));
}

processData('hello', result => {
  console.log('处理结果:', result);
});

// 3. 可以嵌套,但要避免无限循环
process.nextTick(() => {
  console.log('第一次 nextTick');
  process.nextTick(() => {
    console.log('嵌套的 nextTick');
  });
});

process.nextTick 的使用场景

场景一:确保异步执行

javascript
// ❌ 同步调用回调会导致意外行为
function readFileMaybeSync(filename, callback) {
  if (cache.has(filename)) {
    // 问题:这是同步调用,可能在 Promise 解析之前执行
    callback(null, cache.get(filename));
  } else {
    fs.readFile(filename, callback);
  }
}

// ✅ 使用 nextTick 确保回调始终异步执行
function readFileAsync(filename, callback) {
  if (cache.has(filename)) {
    process.nextTick(() => callback(null, cache.get(filename)));
  } else {
    fs.readFile(filename, callback);
  }
}

场景二:在 Promise 构造函数中发出错误

javascript
// ✅ 在 Promise 构造函数中使用 nextTick 发出错误
function promisifyCallback(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      try {
        fn.call(this, ...args, (err, result) => {
          if (err) {
            reject(err);
          } else {
            resolve(result);
          }
        });
      } catch (err) {
        // 使用 nextTick 确保错误在 Promise 链中正确传播
        process.nextTick(() => reject(err));
      }
    });
  };
}

process.nextTick vs Promise 微任务

javascript
// 执行顺序演示
console.log('1. 同步开始');

process.nextTick(() => {
  console.log('2. nextTick 1');
  
  process.nextTick(() => {
    console.log('4. nextTick 嵌套');
  });
  
  Promise.resolve().then(() => {
    console.log('5. nextTick 内的 Promise');
  });
});

Promise.resolve().then(() => {
  console.log('3. Promise 1');
  
  process.nextTick(() => {
    console.log('6. Promise 内的 nextTick');
  });
});

console.log('7. 同步结束');

// 输出顺序:
// 1. 同步开始
// 7. 同步结束
// 2. nextTick 1
// 4. nextTick 嵌套
// 3. Promise 1
// 5. nextTick 内的 Promise
// 6. Promise 内的 nextTick

setImmediate 队列

基本用法

javascript
// setImmediate 在下一个事件循环的 check 阶段执行
setImmediate(() => {
  console.log('setImmediate 执行');
});

// 等价于在 I/O 回调中的 setTimeout(fn, 0)
// 但 setImmediate 效率更高

setImmediate vs setTimeout(0)

javascript
// 在主模块中,顺序不确定
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
// 可能先输出 setTimeout 或 setImmediate(取决于 CPU 性能)

// 在 I/O 回调中,setImmediate 一定先执行
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => console.log('1. setTimeout'), 0);
  setImmediate(() => console.log('2. setImmediate'));
});
// 输出顺序固定:
// 2. setImmediate
// 1. setTimeout

原因分析

code
事件循环阶段:
┌───────────────────────┐
│   timers 阶段          │ ← setTimeout 在这里执行
│   (setTimeout/setInterval)│
├───────────────────────┤
│   I/O callbacks 阶段   │ ← readFile 回调在这里
├───────────────────────┤
│   idle, prepare 阶段   │
├───────────────────────┤
│   poll 阶段           │
├───────────────────────┤
│   check 阶段          │ ← setImmediate 在这里执行
└───────────────────────┘

在 I/O 回调中:
1. timers 阶段已经过了
2. 执行 I/O 回调
3. 进入 check 阶段,执行 setImmediate
4. 下一个循环到达 timers 阶段,执行 setTimeout

微任务队列详解

Promise 微任务

javascript
// Promise.then/catch/finally 产生微任务
Promise.resolve('数据')
  .then(data => {
    console.log('then 1:', data);
    return data + ' + 处理1';
  })
  .then(data => {
    console.log('then 2:', data);
  });

// queueMicrotask 手动添加微任务
queueMicrotask(() => {
  console.log('queueMicrotask');
});

async/await 产生的微任务

javascript
async function asyncFunction() {
  console.log('1. async 函数开始');
  
  await Promise.resolve();
  // await 后面的代码作为微任务执行
  
  console.log('2. await 后的代码');
}

asyncFunction();
console.log('3. 同步代码');

// 输出顺序:
// 1. async 函数开始
// 3. 同步代码
// 2. await 后的代码

宏任务队列详解

定时器宏任务

javascript
// setTimeout 宏任务
setTimeout(() => {
  console.log('setTimeout 0ms');
}, 0);

// setTimeout 实际延迟
setTimeout(() => {
  console.log('实际延迟可能超过 1ms');
}, 1);

// setInterval 宏任务
const id = setInterval(() => {
  console.log('interval');
}, 100);

// 5 秒后停止
setTimeout(() => clearInterval(id), 5000);

I/O 宏任务

javascript
const fs = require('fs');

// 文件 I/O 宏任务
fs.readFile('data.txt', 'utf8', (err, data) => {
  console.log('文件读取完成');
});

// 网络 I/O 宏任务
const http = require('http');
http.get('http://example.com', (res) => {
  console.log('请求完成');
});

Node.js 版本差异

Node.js 11 之前

javascript
// Node.js 11 之前:每个宏任务后执行所有微任务
setTimeout(() => {
  console.log('setTimeout 1');
  Promise.resolve().then(() => console.log('Promise 1'));
}, 0);

setTimeout(() => {
  console.log('setTimeout 2');
  Promise.resolve().then(() => console.log('Promise 2'));
}, 0);

// 输出顺序(Node.js 10 及更早):
// setTimeout 1
// Promise 1
// setTimeout 2
// Promise 2

Node.js 11 之后(当前行为)

javascript
// Node.js 11+:与浏览器行为一致
// 每个宏任务执行后立即执行所有微任务

setTimeout(() => {
  console.log('setTimeout 1');
  Promise.resolve().then(() => console.log('Promise 1'));
}, 0);

setTimeout(() => {
  console.log('setTimeout 2');
  Promise.resolve().then(() => console.log('Promise 2'));
}, 0);

// 输出顺序(Node.js 11+):
// setTimeout 1
// Promise 1
// setTimeout 2
// Promise 2
// (与之前相同,但内部执行逻辑有细微差别)

完整执行顺序示例

javascript
console.log('1. 同步代码开始');

// 宏任务 1
setTimeout(() => {
  console.log('2. setTimeout 1');
  
  // 宏任务 1 中的微任务
  Promise.resolve().then(() => {
    console.log('3. setTimeout 1 的 Promise');
  });
  
  // 宏任务 1 中的 nextTick
  process.nextTick(() => {
    console.log('4. setTimeout 1 的 nextTick');
  });
}, 0);

// 宏任务 2
setImmediate(() => {
  console.log('5. setImmediate 1');
});

// 微任务 1
Promise.resolve().then(() => {
  console.log('6. Promise 1');
  
  // 微任务中嵌套
  process.nextTick(() => {
    console.log('7. Promise 1 的 nextTick');
  });
});

// nextTick 1
process.nextTick(() => {
  console.log('8. nextTick 1');
  
  // nextTick 中嵌套
  Promise.resolve().then(() => {
    console.log('9. nextTick 1 的 Promise');
  });
});

console.log('10. 同步代码结束');

// 输出顺序(分析):
// 1. 同步代码开始
// 10. 同步代码结束
// 8. nextTick 1
// 9. nextTick 1 的 Promise(nextTick 队列清空后执行 Promise 微任务)
// 6. Promise 1
// 7. Promise 1 的 nextTick
// 2. setTimeout 1
// 4. setTimeout 1 的 nextTick
// 3. setTimeout 1 的 Promise
// 5. setImmediate 1

实战案例

案例 1:理解执行顺序

javascript
async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

setTimeout(() => {
  console.log('setTimeout');
}, 0);

async1();

new Promise((resolve) => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});

console.log('script end');

// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout

案例 2:避免阻塞事件循环

javascript
// ❌ 错误:同步处理大量数据会阻塞
function processDataSync(data) {
  const results = [];
  for (let i = 0; i < data.length; i++) {
    results.push(heavyCalculation(data[i]));
  }
  return results;
}

// ✅ 正确:使用 setImmediate 分批处理
async function processDataAsync(data, chunkSize = 100) {
  const results = [];
  
  for (let i = 0; i < data.length; i += chunkSize) {
    const chunk = data.slice(i, i + chunkSize);
    
    // 使用 setImmediate 让出控制权
    await new Promise(resolve => setImmediate(resolve));
    
    for (const item of chunk) {
      results.push(heavyCalculation(item));
    }
  }
  
  return results;
}

案例 3:实现异步队列

javascript
class AsyncQueue {
  constructor() {
    this.queue = [];
    this.processing = false;
  }
  
  enqueue(task) {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.processQueue();
    });
  }
  
  async processQueue() {
    if (this.processing || this.queue.length === 0) {
      return;
    }
    
    this.processing = true;
    
    while (this.queue.length > 0) {
      const { task, resolve, reject } = this.queue.shift();
      
      try {
        const result = await task();
        resolve(result);
      } catch (error) {
        reject(error);
      }
      
      // 使用 setImmediate 让出控制权
      await new Promise(resolve => setImmediate(resolve));
    }
    
    this.processing = false;
  }
}

// 使用示例
const queue = new AsyncQueue();

queue.enqueue(() => fetch('url1')).then(console.log);
queue.enqueue(() => fetch('url2')).then(console.log);
queue.enqueue(() => fetch('url3')).then(console.log);

最佳实践

1. 优先使用 Promise API

javascript
// ❌ 混用回调和 Promise
fs.readFile('data.txt', (err, data) => {
  if (err) throw err;
  Promise.resolve(data).then(/* ... */);
});

// ✅ 统一使用 Promise
const fs = require('fs').promises;
const data = await fs.readFile('data.txt', 'utf8');

2. 避免过度使用 process.nextTick

javascript
// ❌ 可能导致 I/O 饥饿
function recursiveNextTick() {
  process.nextTick(recursiveNextTick);
}

// ✅ 使用 setImmediate 让 I/O 有机会执行
function recursiveImmediate() {
  setImmediate(recursiveImmediate);
}

3. 理解事件循环阶段

javascript
// 在不同阶段执行任务的选择:
// 1. 需要最高优先级 → process.nextTick
// 2. 需要在当前栈清空后立即执行 → Promise
// 3. 需要在下一个事件循环 → setImmediate
// 4. 需要延迟执行 → setTimeout

小结

  • Node.js 有三个主要任务队列:nextTick、微任务、宏任务
  • process.nextTick 是 Node.js 特有,优先级最高
  • setImmediatesetTimeout(fn, 0) 在 I/O 回调中更可靠
  • Node.js 11+ 与浏览器的事件循环行为基本一致
  • 避免过度使用 process.nextTick,可能导致 I/O 饥饿
  • 理解任务队列顺序对于调试异步代码至关重要