在上一篇文章理解NodeJs中的Event Loop、Timers以及process.nextTick中笔者提了几个问题,如今针对这些问题给出个人理解,若有错漏烦请指正。html
若是你对NodeJs系列感兴趣,欢迎关注微信公众号:前端神盾局或 github NodeJs系列文章前端
在上一篇文章中提到在poll阶段会“接收新的I/O事件而且在适当时node会阻塞在这里”,那什么状况下会阻塞呢?阻塞多久呢?node
对于这个问题,咱们必须深刻到libuv的源码,看看poll阶段是怎么实现的:c++
int uv_run(uv_loop_t* loop, uv_run_mode mode) { int timeout; int r; int ran_pending; r = uv__loop_alive(loop); if (!r) uv__update_time(loop); while (r != 0 && loop->stop_flag == 0) { uv__update_time(loop); uv__run_timers(loop); ran_pending = uv__run_pending(loop); uv__run_idle(loop); uv__run_prepare(loop); timeout = 0; if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT) timeout = uv_backend_timeout(loop); // 这是poll阶段 uv__io_poll(loop, timeout); uv__run_check(loop); uv__run_closing_handles(loop); if (mode == UV_RUN_ONCE) { /* UV_RUN_ONCE implies forward progress: at least one callback must have * been invoked when it returns. uv__io_poll() can return without doing * I/O (meaning: no callbacks) when its timeout expires - which means we * have pending timers that satisfy the forward progress constraint. * * UV_RUN_NOWAIT makes no guarantees about progress so it's omitted from * the check. */ uv__update_time(loop); uv__run_timers(loop); } r = uv__loop_alive(loop); if (mode == UV_RUN_ONCE || mode == UV_RUN_NOWAIT) break; } /* The if statement lets gcc compile it to a conditional store. Avoids * dirtying a cache line. */ if (loop->stop_flag != 0) loop->stop_flag = 0; return r; }
从源码咱们能够看到uv__io_poll
传入了timeout
做为参数,而这个timeout
就决定了poll阶段阻塞的时长,明白这一点咱们就能够把问题转化成:是什么决定的timeout
的值?git
再回到源码中,timeout
的初始值为0,也就意味着poll阶段以后会直接转入check阶段而不会发生阻塞。可是当(mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT
这些条件成立时,timeout
就由uv_backend_timeout
的返回值决定。github
这里须要插播一下关于mode
值的问题,根据官方文档 mode
一共有三种状况:c#
UV_RUN_DEFAULT
UV_RUN_ONCE
UV_RUN_NOWAIT
这里咱们只关心UV_RUN_DEFAULT
,由于Node event loop使用的是这种模式.微信
OK~回到问题,咱们再看一下uv_backend_timeout
会返回什么?异步
int uv_backend_timeout(const uv_loop_t* loop) { if (loop->stop_flag != 0) return 0; if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop)) return 0; if (!QUEUE_EMPTY(&loop->idle_handles)) return 0; if (!QUEUE_EMPTY(&loop->pending_queue)) return 0; if (loop->closing_handles) return 0; return uv__next_timeout(loop); }
这是一个多步条件判断函数,咱们一个个分析:函数
uv_stop()
,stop_flag != 0
),timeout
为0timeout
为0idle_handles
和pending_queue
,timeout
为0(对于idle_handles
和pending_queue
分别表明什么,笔者尚未概念,若是后面有相应资料会及时更新)timeout
为0uv__next_timeout
处理int uv__next_timeout(const uv_loop_t* loop) { const struct heap_node* heap_node; const uv_timer_t* handle; uint64_t diff; heap_node = heap_min((const struct heap*) &loop->timer_heap); if (heap_node == NULL) return -1; /* block indefinitely */ handle = container_of(heap_node, uv_timer_t, heap_node); if (handle->timeout <= loop->time) return 0; // 这句代码给出了关键性的指导 // 对比当前loop的时间戳 diff = handle->timeout - loop->time; //不能大于最大的INT_MAX if (diff > INT_MAX) diff = INT_MAX; return diff; }
总结一下,event loop 知足如下条件时,poll阶段会进行阻塞:
而阻塞的时间最长不超过给定定时器的最小阀值
setTimeout
和setImmediate
的执行顺序是不必定的?上文提到setTimeout
和setImmediate
在非I/O循环中,执行顺序是不必定的,好比:
setTimeout(function timeout() { console.log('timeout'); }, 0); setImmediate(function immediate() { console.log('immediate'); });
$ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout
相同代码,两次运行结果倒是相反的,这是为何呢?
在node中,setTimeout(cb, 0) === setTimeout(cb, 1)
在event loop的第一个阶段(timers阶段),node都会从一堆定时器中取出一个最小阀值的定时器来与loop->time
进行比较,若是阀值小于等于loop->time
表示定时器已超时,相应的回调便会执行(随后会检查下一个定时器),若是没有则会进入下一个阶段。
因此setTimeout
是否在第一阶段执行取决于loop->time
的大小,这里可能出现两种状况:
loop->time >=1
,则uv_run_timer
生效,timeout
先执行loop->time < 1
,则本次loop中的第一次uv_run_timer
不生效,那么io_poll
后先执行uv_run_check
,即immediate
先执行,而后等close cb
执行完后,继续执行uv_run_timer
这就是为何同一段代码,执行结果随机的缘故。那为何说在I/O回调中,必定是先immediate
执行呢,其实也很容易理解,考虑如下场景:
// timeout_vs_immediate.js const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
因为timeout
和immediate
的事件注册是在readFile
的回调执行时触发的,因此必然的,在readFile
的回调执行前的每一次event loop进来的uv_run_timer
都不会有超时事件触发
那么当readFile
执行完毕,poll阶段收到监听的fd事件完成后,执行了该回调,此时
timeout
事件注册immediate
事件注册readFile
的回调执行完毕,那么就会从uv_io_poll
中出来,此时当即执行uv_run_check
,因此immediate
事件被执行掉uv_run_timer
检查timeout
事件,执行timeout
事件因此你会发现,在I/O回调中注册的二者,永远都是immediately
先执行
栈展开主要是指在抛出异常后逐层匹配catch语句的过程,举个例子:
function a(){ b(); } function b(){ c(); } function c(){ throw new Error('from function c'); } a();
这个例子中,函数c
抛出异常,这是首先会在c
函数自己检查是否存在try相关的catch语句,若是没有就退出当前函数,而且释放当前函数的内存并销毁局部对象,继续到b
函数中查找,这个过程就称之为栈展开。