微任务与宏任务

在 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 完成。

基本用法

使用 setTimeoutsetImmediate 或 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 延迟)。
  • 执行顺序

    1. 执行同步代码。
    2. 清空 Next Tick Queue。
    3. 清空 Microtask Queue。
    4. 进入下一个宏任务阶段(e.g., timers)。
    5. 重复 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,确保非阻塞。

相关推荐
IT_陈寒2 小时前
Redis 性能提升秘籍:这5个被低估的命令让你的QPS飙升200%
前端·人工智能·后端
多看书少吃饭2 小时前
前端实现抽烟识别:从算法到可视化
前端·算法
excel2 小时前
合并路由与微前端框架的对比解析
前端
aesthetician2 小时前
clsx:高效处理 React 条件类名的实用工具
前端·react.js·前端框架
粉末的沉淀3 小时前
css:固定跨度间隔的渐变色设置
前端·css
阿正的梦工坊3 小时前
Mac电脑解决 npm 和 Yarn 安装时的证书过期问题
前端·macos·npm
2503_928411566 小时前
9.26 数据可视化
前端·javascript·信息可视化·html5
我叫唧唧波6 小时前
【打包工具】webpack基础
前端·webpack
知识分享小能手8 小时前
React学习教程,从入门到精通,React 单元测试:语法知识点及使用方法详解(30)
前端·javascript·vue.js·学习·react.js·单元测试·前端框架