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 1process.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 内的 nextTicksetImmediate 队列
基本用法
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 2Node.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 特有,优先级最高setImmediate比setTimeout(fn, 0)在 I/O 回调中更可靠- Node.js 11+ 与浏览器的事件循环行为基本一致
- 避免过度使用
process.nextTick,可能导致 I/O 饥饿 - 理解任务队列顺序对于调试异步代码至关重要