Open MandyJin opened 6 years ago
nodejs是单线程(single thread)运行的,通过一个事件循环(event-loop)来循环取出消息队列(event-queue)中的消息进行处理,处理过程基本上就是去调用该消息对应的回调函数。消息队列就是当一个事件状态发生变化时,就将一个消息压入队列中。
nodejs的事件驱动模型一般要注意下面几个点:
javascript 有两个任务队列:
task
包括:script(整体代码),setTimeout,setInterval,setImmediate,I/O,UI rendering.
jobs
包括:process.nextTick,Promise,Object.observe(已废弃),MutationObserver(监视DOM变动的接口).
事件循环的顺序,决定了javascript代码的执行顺序。它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈(消息队列?)。直到调用栈清空(只剩全局),然后执行所有的 jobs。当所有可执行的 jobs 执行完毕之后,循环再次从 task 开始,找到其中一个任务队列执行完毕,然后再执行所有的 jobs,这样一直循环下去。
node和chrome js执行引擎都是以V8为核心的。因此,在异步事件处理上十分相似。 在node中,异步分为两种,一种是IO的异步,一种是非IO的异步。 异步IO有磁盘IO和网络IO,非IO异步有process.nextTick, promise, setTimeout, setImmediate.。 非异步IO中,process.nextTick, promise等属于microTask, setTimeout, setImmediate 是macroTask。 异步IO的调用需要底层libuv层支持,较为复杂,属于macroTask。
比如,在js中发起一个网络请求时,js代码调用node核心模块,核心模块再调用C++内建模块。内建模块通过libuv进行系统调用。系统调用的过程中,创建相应的请求对象,从js层传入的参数和当前方法都被封装在请求对象中,回调函数被设置在请求对象的oncomplete_sym上,对象包装完毕后,调用QueueUserWorkItem将请求对象推入线程池中等待执行。
js层继续执行后续执行栈中的代码。
在内核层,当线程池中有空闲线程时,调用相应底层函数,发起网络请求。
此时,JavaScript调用立即返回。根据当前的执行栈,继续执行原执行栈的函数。检查microTask中是否任务可以执行,若有,则按照优先级、调用先后顺序依次执行,直到将microTask执行结束。
网络请求对象送入IO线程池等待执行,内核利用端口发起真正的网络请求,并侦听网络端口,一旦接收到网络请求,表示内核层面的网络请求执行完毕。
执行完毕后,会将结果储存在请求对象的result属性上。window下调用PostQueuedCompletionStatus通知IOCP,告知当前对象操作已经完成,并将线程归还线程池。
内核发送IO观察者形成事件,每次Tick时,调用GetQueuedCompletionStatus获取执行状态,得到执行完的IO操作,将请求对象加入IO观察者队列中,将其当作事件处理。
网络IO属于macroTask,这个macroTask放入event loop中。event loop中的异步IO都有相应的观察者,每次tick,检查优先级较高的观察者。
process.nextTick 属于idle观察者,setImmediate是check观察者。idle观察者优于IO观察者,IO观察者优于check观察者。
如有IO观察者回调函数,取出请求对象的result作为参数,在js层回调执行,结束调用。
然后检查microTask,执行所有microTask函数。同时,microTask在event loop中也有优先级。