在 Node.js 中,微任务(Microtasks)和宏任务(Macrotasks)是事件循环(Event Loop)的核心组成部分,用于管理异步操作的执行顺序。这两种任务机制源于 JavaScript 的单线程模型,通过 libuv 库的底层支持,实现非阻塞 I/O 和高效任务调度。微任务和宏任务的区分允许开发人员精确控制异步代码的优先级,避免事件循环饥饿,并确保关键操作(如 Promise 解析)在宏任务间及时执行。这在处理高并发、网络请求或实时计算时特别有用。
基本概念
Node.js 的事件循环基于 libuv 库,实现了一个阶段化的循环模型,用于处理异步任务。事件循环不是 JavaScript V8 引擎的一部分,而是 Node.js 运行时通过 C++ 实现的。循环分为多个阶段(如 timers、poll、check),每个阶段处理特定类型的宏任务。
- 宏任务(Macrotasks):对应事件循环的"主要任务",在特定阶段执行,通常涉及 I/O 或定时器。宏任务队列由 libuv 管理。
- 微任务(Microtasks):更高优先级的"子任务",在每个宏任务后或事件循环阶段间执行。微任务队列由 V8 引擎和 Node.js 共同管理。
事件循环的执行顺序:同步代码 → 微任务队列 → 宏任务阶段(循环)。如果微任务不断添加新微任务,可能导致宏任务"饥饿"。
Node.js 提供了两个微任务队列:Next Tick Queue(process.nextTick)和 Microtask Queue(Promise callbacks)。
微任务:高优先级异步执行
微任务用于在当前宏任务结束后立即执行异步代码,确保优先级高于宏任务。微任务队列在每个事件循环阶段后清空。
基本用法
使用 process.nextTick
或 Promise:
javascript
console.log('Start');
process.nextTick(() => {
console.log('Next Tick');
});
Promise.resolve().then(() => {
console.log('Promise Then');
});
console.log('End');
// 输出: Start -> End -> Next Tick -> Promise Then
这里,同步代码先执行,然后是 Next Tick Queue,最后是 Microtask Queue。
自定义微任务
微任务通常通过内置 API 实现,但你可以链式使用:
javascript
process.nextTick(() => {
console.log('Tick 1');
process.nextTick(() => console.log('Tick 2'));
});
Promise.resolve().then(() => {
console.log('Then 1');
Promise.resolve().then(() => console.log('Then 2'));
});
Next Tick 优先于 Promise then 执行,因为 Next Tick Queue 在 Microtask Queue 前清空。
微任务的关键特性:递归执行直到队列为空,避免阻塞事件循环。
宏任务:事件循环阶段任务
宏任务对应 libuv 的事件循环阶段,如定时器回调或 I/O 完成。
基本用法
使用 setTimeout
、setImmediate
或 I/O:
javascript
setTimeout(() => console.log('Timeout'), 0);
setImmediate(() => console.log('Immediate'));
fs.readFile('file.txt', (err, data) => {
console.log('File Read');
});
这些在 timers、check 或 poll 阶段执行。
自定义宏任务
宏任务通常由 libuv API 调度,如自定义定时器或 socket 事件。
宏任务的关键:阶段化执行,确保 I/O 非阻塞。
区别:优先级与执行顺序
-
定义:
- 微任务:小粒度、高优先级任务,如 process.nextTick、Promise.then、async/await、MutationObserver(浏览器侧)。
- 宏任务:大粒度任务,如 setTimeout、setInterval、setImmediate、I/O callbacks、UI 渲染(浏览器侧)。
-
区别:
- 优先级:微任务在每个宏任务后执行,所有微任务清空后才进入下一个宏任务。
- 队列:微任务有两个队列(Next Tick > Microtask);宏任务分散在 libuv 阶段队列中。
- 执行时机:微任务在 JS 栈清空时执行,可能在宏任务内多次运行;宏任务在事件循环特定阶段。
- 潜在问题:无限微任务可能导致宏任务饥饿(如 I/O 延迟)。
-
执行顺序:
- 执行同步代码。
- 清空 Next Tick Queue。
- 清空 Microtask Queue。
- 进入下一个宏任务阶段(e.g., timers)。
- 重复 2-3。 示例:
javascript
console.log('Sync 1');
setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('Next Tick'));
console.log('Sync 2');
// 输出: Sync 1 -> Sync 2 -> Next Tick -> Promise -> Timeout
使用场景
-
微任务场景:
- 立即异步:process.nextTick 用于在当前栈后执行,避免阻塞(如递归 setTimeout 的替代)。
- Promise 链:确保 then/catch 在 resolve 后立即执行,支持 async/await。
- 错误处理:捕获微任务中的错误而不中断宏任务。
- 示例:数据库事务后立即更新缓存。
-
宏任务场景:
- 定时任务:setTimeout 用于延迟执行。
- I/O 操作:文件读写、网络请求。
- 批处理:setImmediate 用于在 poll 阶段后执行,避免阻塞 I/O。
- 示例:UI 更新后渲染(浏览器),或服务器响应后日志。
为什么要有这种区分?微任务允许细粒度控制,确保关键异步逻辑(如状态更新)优先;宏任务防止单线程阻塞,确保 I/O 公平调度。区分避免了所有任务同级导致的混乱,提高了异步代码的可预测性。
深入原理:libuv 底层原理
Node.js 的事件循环由 libuv 提供 C++ 实现,Node.js 在其上添加微任务支持。libuv 的 uv_run() 函数驱动循环,Node.js 通过 V8 集成微任务队列。以下通过 Node.js 源码展示原理。
libuv 事件循环核心
libuv 的事件循环在 uv_run(uv_loop_t* loop, uv_run_mode mode) 中实现(libuv/src/unix/core.c 或 win/core.c):
c
// 简化伪代码 from libuv src
int uv_run(uv_loop_t *loop, uv_run_mode mode) {
while (r != 0 && loop->stop_flag == 0) {
uv__update_time(loop); // 更新时间
uv__run_timers(loop); // timers 阶段
r = uv__run_pending(loop); // pending callbacks
uv__run_idle(loop); // idle
uv__run_prepare(loop); // prepare
uv__io_poll(loop, timeout); // poll 阶段
uv__run_check(loop); // check (setImmediate)
uv__run_closing_handles(loop); // close
}
return r;
}
libuv 处理宏任务阶段:timers (setTimeout)、poll (I/O)、check (setImmediate) 等。Node.js 包装此循环,插入微任务执行。
Node.js 集成微任务
在 Node.js src/node.cc 中,Run() 函数包装 uv_run,并运行微任务:
cpp
// 从 Node.js src/node.cc 简化
int NodeMainInstance::Run() {
Isolate* isolate = env_->isolate();
while (true) {
bool platform_finished = false;
uv_run(env_->event_loop(), UV_RUN_ONCE); // 执行一个 libuv 迭代(宏任务阶段)
// 执行平台任务 (e.g., V8 tasks)
platform_finished = !v8_platform->PumpMessageLoop(isolate_data_, isolate);
// 执行 Next Tick Queue 和 Microtask Queue
env_->RunAndClearNativeImmediates(); // Next Tick (部分)
if (EmitProcessBeforeExit(env_)) continue;
v8::MicrotasksPolicy policy = v8::MicrotasksPolicy::kExplicit;
isolate->RunMicrotasks(); // 执行 V8 Microtask Queue (Promise)
// 处理 Node.js Next Tick Queue
if (env_->TickInfo()->HasScheduledTasks()) {
env_->RunAndClearNextTicks(); // 清空 Next Tick Queue
}
if (platform_finished && !env_->TickInfo()->HasScheduledTasks()) break;
}
return exit_code_;
}
- uv_run(UV_RUN_ONCE):执行一个宏任务阶段。
- isolate->RunMicrotasks():调用 V8 的微任务队列(Promise callbacks)。
- env_->RunAndClearNextTicks():处理 Node.js 特有的 Next Tick Queue(process.nextTick)。
Next Tick Queue 优先于 V8 Microtask Queue,因为它在 RunMicrotasks 前或后检查。

Next Tick Queue 源码
在 lib/internal/process/task_queues.js 中,实现 nextTick Queue:
javascript
// 从 Node.js lib/internal/process/task_queues.js
const { nextTickQueue } = internalBinding('task_queue');
function processTicksAndRejections() {
let tock;
do {
while (tock = nextTickQueue.shift()) {
// 执行 nextTick 回调
const asyncId = tock[async_id_symbol];
emitBefore(asyncId, tock[trigger_async_id_symbol]);
try {
const callback = tock.callback;
if (tock.args === undefined) {
callback();
} else {
Reflect.apply(callback, undefined, tock.args);
}
} finally {
emitAfter(asyncId);
}
}
runMicrotaskQueue(); // 调用 V8 微任务
} while (!nextTickQueue.isEmpty() || hasMicrotasks());
}
- nextTickQueue:一个数组,push nextTick 回调。
- processTicksAndRejections():递归清空队列,先 Next Tick,后 Microtasks (runMicrotaskQueue 调用 V8 RunMicrotasks)。
在 lib/internal/process/next_tick.js:
javascript
function nextTick(callback) {
// 验证 callback
if (typeof callback !== 'function') throw new TypeError('callback is not a function');
// push 到队列
nextTickQueue.push(callback);
// 如果需要,调度 tick
if (!ticking) {
setTickScheduled(true);
scheduleMicrotask(); // 通过 V8 调度
}
}
为什么区分?
从源码可见,微任务(Next Tick + Microtasks)在每个 uv_run 迭代后执行,确保高优先级任务(如 Promise 解析)不被宏任务(如长 I/O)延迟。宏任务阶段化(libuv)防止单线程死锁。区分允许:
- 优先微任务:支持 async/await 的"同步"语义。
- 避免饥饿:宏任务确保 I/O 进展。
- 性能:微任务递归清空,宏任务分阶段。
如果无区分,所有任务同队列,可能导致优先级混乱或循环阻塞。
总结
微任务和宏任务是 Node.js 异步模型的基石,从简单用法到自定义调度,由浅入深地提升代码控制力。通过事件循环的阶段化和队列管理,我们可以构建高效应用。底层 libuv 与 V8 的集成,通过 uv_run 和 RunMicrotasks,确保非阻塞。