该文章阅读需要5分钟,更多文章请点击本人博客halu886
事件循环
正是事件循环,所以在Node中回调函数如此普遍。
当进程启动时,Node会创建一个while(true)
的循环,每执行一次循环称为一个Tick。Tick的过程首先判断是否有事件,如果有事件则取出事件,且判断判断是否有关联回调,执行关联回调。如果没有事件则退出进程。
观察者
每个Tick如何判断是否有事件需要被处理呢?这里就需要引入观察者的概念了。 每个事件循环都有一个或多个监听者,判断当前Tick是否事件需要处理就是询问这些监听者。
浏览器的也存在监听者,例如一次点击或者加载一个文件。Node的事件则是网络请求,文件I/O。并且每种不同的事件都有对应的观察者。观察者将事件进行了分类。
事件循环是一个生产者/消费者模型。异步I/O,网络请求负责生产事件,事件传递到监听者中,事件循环则从观察者中取出事件并拓展。
在Windows中,这个循环基于IOCP创建,在*nix中,则是基于多线程创建。
请求对象
接下来我们将要举个例子来说明 在Windows中(基于IOCP)异步I/O从Javascript层到内核发生了什么。
对于一般的非异步I/O回调函数,有我们自行调用。
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()
中的回调函数是是如何执行和调用的。
fs.open = function(path,flags,mode,callback){
//...
binding.open(pathModule.makeLong(path),
stringToFlags(flags),
mode,
callback);
}
fs.open()
的功能是指定文件路径和参数打开一个文件,获取一个文件描述符,这也是后续所有异步I/O的初始操作。其实Javascript层面是调用C++核心模块,获取文件描述符。
从Javascript调用Node核心模块,核心模块调用C++内建模块,内建模块通过libuv系统调用,这是典型的Node的调用堆栈。libuv作为封装层,实际上调用uv_fs_open()方法。调用过程中,创建了一个FSReqWrap请求对象,所有参数和当前方法都封装在这个对象中,回调函数则被设置在这个对象中的oncomplete_sym属性中。
req_wrap->object->Set(oncomplete_sym,callback);
对象包装完毕,在Windows中调用QueueUserWorkItem()
把这个对象推入线程池中等待执行
QueueUserWorkItem(&uv_fs_thread_proc,
req,
WT_EXECUTEDEFAULT)
QueueUserWorkItem()
会接受三个参数,第一个参数是要被执行的方法的引用,第二个则是引用的方法的参数,第三个参数则是执行的标志。当线程池中有可用的参数时,uv_fs_thread_proc()
则会通过参数的类型调用相应的底层函数,例如uv_fs_open()
,其实调用的是fs_open()
。
至此,Javascript层面的异步I/O至此结束,然后Javascript线程接着执行当前任务的后续操作。当线程池中有空闲的线程时则执行I/O且不会阻塞Javascript线程的后续执行。
请求对象是异步I/O的重要中间产物,所有状态都保存在这个请求对象中,包括送入线程池以及I/O操纵结束的回调函数执行。
执行回调
组装好请求对象,将对象送入I/O线程池等待执行,这才是第一步。第二步才是执行回调。
线程I/O完成操作后,会将结果存储在req->result上。然后调用PostQueueCompletionStatus()
通知IOCP,告知当前对象操作已经完成:
PostQueuedCompletionStatus((loop)->iocp,o,o,&((req)->overlapped))
PostQueuedCompletionStatus()
方法的作用就是向IOCP提交执行状态,并将线程返还给线程池。
且在每次Tick的执行中,会调用IOCP的GetQueuedCompletetionStatus()
检查线程池是否有执行完的请求。如果存在则将请求对象加入到I/O观察者的队列中,然后当作事件处理。
以上知识点均来自<<深入浅出Node.js>>,更多细节建议阅读书籍:-)