3.3.4 执行回调

36 阅读2分钟

这一子节是整个异步I/O过程的完美收尾:I/O操作完成后,回调函数到底是怎么被触发并执行的?朴灵作者在这里把前面的“事件循环 → 观察者 → 请求对象”全部串起来,形成一个完整的闭环。

执行回调的全过程

当底层I/O完成(线程池或系统异步通知)后,libuv会做以下事情:

  1. 填充请求对象:把结果(读取的数据、错误码等)写入请求对象(uv_fs_t 等)。
  2. 回调包装:libuv调用预先注册的C++回调函数(例如 uv__fs_done)。
  3. 推入事件队列:C++回调函数把JS层的回调函数包装成一个任务,放入事件循环的相应阶段队列(通常是 poll 阶段的 I/O 回调队列)。
  4. 事件循环触发
    • 事件循环进入 poll 阶段。
    • 发现有就绪的 I/O 观察者。
    • 执行关联的 I/O 回调(MakeCallback)。
    • 最终在 V8 中调用用户传入的 JavaScript 回调函数(callback(err, data))。

关键细节

  • 线程安全:线程池中的工作线程不能直接调用 V8(V8 是单线程的),所以完成时只会把任务“邮寄”回主线程(事件循环线程)。
  • 批量执行:一次 poll 可能处理多个就绪的 I/O 回调,提高效率。
  • 异常处理:如果回调抛错,会触发 process 的 uncaughtException 事件(或 domain,如果用了)。

作者用一个完整流程图总结(书中最经典的一张图):

JS 调用 fs.readFile

C++ 层创建请求对象(uv_fs_t)

libuv 投递到线程池(文件I/O)或系统异步(网络)

I/O 完成 → libuv 填充结果 → 调用 C++ 回调

C++ 回调把 JS 回调推入 poll 阶段队列

事件循环 poll 阶段执行 → V8 调用用户回调

下面是一些经典的“执行回调阶段图”、完整异步I/O闭环图、libuv回调触发示意图(这些是读者们读完这一节后最常分享的总结图):

小结

读完 3.3.1 ~ 3.3.4 这四个子节,你就彻底掌握了 Node 异步I/O 的底层原理:

  • 事件循环是心脏
  • 观察者是哨兵
  • 请求对象是快递包裹
  • 执行回调是最终派送

很多人读完这里感叹:“原来回调是这样回来的!Node 单线程却能高并发的秘密全在这里了。”