🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
在这一节中,我们将通过解释Windows下异步I/O(利用IOCP实现)的简单例子来探寻从JavaScript代码到系统内核之间都发生了什么。 对于一般的(非异步)回调函数,函数由我们自行调用,如下所示: ~~~ var forEach = function(list, callback){ for(var i=0;i<list.length;i++){ callback(list[i],i,list); } }; ~~~ 对于Node中的异步I/O调用而言,回调函数却不由开发者来调用。那么从我们发出调用后,到回调函数被执行,中间发生了什么呢?事实上,从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,它叫做请求对象。 下面我们以最简单的fs.open()方法来作例子,探索Node与底层之间是如何执行异步I/O调用以及回调函数究竟是如何被调用执行的: ~~~ fs.open=function(path, flags, mode, callback){ //... binding.open(pathModule._makeLong(path), stringToFlags(flags), mode, callback); }; ~~~ fs.open()的作用是根据指定路径和参数去打开一个文件,从而得到一个文件描述符,这是后续所有I/O操作的初始操作。从前面的代码中可以看到,JavaScript层面的代码通过调用C++核心模块进行下层的操作。下图为调用示意图: ![](https://box.kancloud.cn/2016-08-28_57c1cec2afee1.png) 从JavaScript调用Node的核心模块,核心模块调用C++内建模块,内建模块通过libuv进行系统调用,这是Node里经典的调用方式。这里libuv作为封装层,有两个平台实现,实质上是调用了uv_fs_open()方法。在uv_fs_open()的调用过程中,我们创建了一个FSReqWrap请求对象。从JavaScript层传入的参数和当前方法都被封装在这个请求对象中,其中我们最为关注的回调函数则被设置在这个对象的oncomplete_sym属性上: ~~~ req_wrap->object->Set(oncomplete_sym, callback); ~~~ 对象包装完毕后,在Windows下,则调用QueueUserWorkItem()方法将这个FSReqWrap对象推入线程池中等待执行,该方法的代码如下所示: ~~~ QueueUserWorkItem(&uv_fs_thread_proc, \ req, \ WT_EXECUTEDEFAULT) ~~~ QueueUserWorkItem()方法接受3个参数:第一个参数是将要执行的方法的引用,这里引用的是uv_fs_thread_proc,第二个参数是uv_fs_thread_proc方法运行时所需要的参数;第三个参数是执行的标志。当线程池中有可用线程时,我们会调用uv_fs_thread_proc()方法。uv_fs_thread_proc()方法会根据传入的参数的类型调用相应的底层函数。以uv_fs_open()为例,实际上调用fs_open()方法。 至此,JavaScript调用立即返回,由JavaScript层面发起的异步调用的第一阶段就此结束。JavaScript线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到JavaScript线程的后续执行,如此就达到了异步的目的。 请求对象是异步I/O过程中的重要中间产物,所有的状态都保存在这个对象中,包括送入线程池等待执行以及I/O操作完毕后的回调处理。