这一子节是揭开Node异步I/O“黑盒”的关键部分。朴灵作者在这里详细解释了:当你调用一个异步API(如fs.readFile)时,JS层和底层之间到底传递了什么?答案就是请求对象(Request Object)——它像一个“快递包裹”,从JavaScript层封装请求参数和回调,传递给C++层,再到libuv,最终完成I/O后带着结果回来。
请求对象的角色
- JS层无法直接操作底层系统调用(如read()),必须通过C++绑定层桥接。
- 请求对象就是这个桥梁:一个C++结构体(libuv中的uv_req_t或其子类,如uv_fs_t、uv_getaddrinfo_t等)。
- 它封装了:
- 请求类型(文件读写、网络、DNS等)
- 参数(文件路径、缓冲区、回调函数指针)
- 结果(I/O完成后的数据、错误码)
- 回调信息
一个典型流程(以fs.readFile为例)
- JS层:调用fs.readFile(path, callback),V8把参数和回调函数准备好。
- C++绑定层(Node源码的fs模块):创建一个uv_fs_t请求对象(继承自uv_req_t)。
- 填充路径、flags等。
- 把JS回调包装成C++函数(uv_fs_cb)。
- 投递给libuv:调用uv_fs_read()等,把请求对象交给线程池或系统异步API。
- I/O执行:如果是文件I/O(不支持系统异步),进libuv默认线程池(4个线程,可配置);网络/DNS等可能直接系统异步。
- I/O完成:libuv填充请求对象的结果,推入完成队列。
- 回调触发:事件循环poll阶段取到,执行C++回调,再转回JS回调。
作者强调:请求对象是JavaScript与底层异步I/O之间的纽带,它让单线程的JS能安全地发起和接收异步操作,而不阻塞事件循环。
关键点:
- 不所有I/O都进线程池:网络I/O(如TCP)在Unix用epoll/kqueue直接异步,Windows用IOCP;文件I/O几乎都进线程池(因为大多数OS文件异步不完善)。
- 请求对象在堆上分配,避免栈溢出。
这一子节读完,你就彻底明白为什么Node的异步这么高效:请求对象巧妙地桥接了JS和底层,避免了阻塞。