作者在上一篇文章UnderstandingEventLoop,Timersandprocess.nextTickinNodeJs中问了几个问题,现在针对这些问题给出我的理解,如有错误或遗漏请指正。对NodeJs系列感兴趣的朋友请关注微信公众号:前端神盾局poll期什么时候开始?还是githubNodeJs系列文章被封?上一篇文章中提到在poll阶段,“会接收到新的I/O事件,节点会在适当的时候阻塞在这里”,那么什么情况下会阻塞呢?多久会被封?对于这个问题,我们必须深入libuv的源码,??看看poll阶段是如何实现的:intuv_run(uv_loop_t*loop,uv_run_modemode)诠释;intran_pending;r=uv__loop_alive(loop);如果(!r)uv__update_time(循环);while(r!=0&&loop->stop_flag==0){uv__update_time(loop);uv__run_timers(循环);ran_pending=uv__run_pending(循环);uv__run_idle(循环);;超时=0;如果((mode==UV_RUN_ONCE&&!ran_pending)||mode==UV_RUN_DEFAULT)timeout=uv_backend_timeout(loop);//这是轮询阶段uv__io_poll(loop,timeout);uv__run_check(循环);uv__run_closing_handles(循环);if(mode==UV_RUN_ONCE){/*UV_RUN_ONCE意味着向前推进:至少一个回调必须在返回时被调用。uv__io_poll()可以在不做*I/O的情况下返回(意思是:没有回调)当它的超时到期时-这意味着我们*有满足前进进度约束的挂起计时器。**UV_RUN_NOWAIT不保证进度,因此从*检查中省略。*/uv__update_time(loop);;}r=uv__loop_alive(loop);如果(模式==UV_RUN_ONCE||模式==UV_RUN_NOWAIT)中断;}/*if语句让gcc将其编译为条件存储。避免*弄脏缓存行。*/if(loop->stop_flag!=0)loop->stop_flag=0;returnr;}从源码可以看出,uv__io_poll传入了timeout作为参数,这个timeout决定了poll阶段阻塞的时长。我们理解这个问题可以转化为:什么决定了timeout的值?回到源码,timeout的初始值为0,意思是poll阶段结束后,会直接进入check阶段,不会阻塞,但是当(mode==UV_RUN_ONCE&&!ran_pending)||mode==UV_RUN_DEFAULT这些条件成立时,超时由uv_backend_timeout的返回值决定。这里我们需要插入一个关于模式值的问题。根据官方文档,一共有三种模式:UV_RUN_DEFAULTUV_RUN_ONCEUV_RUN_NOWAIT这里我们只关心UV_RUN_DEFAULT,因为Node事件循环使用的就是这种模式。OK~回到问题,我们来看看uv_backend_timeout函数返回什么?intuv_backend_timeout(constuv_loop_t*loop){if(loop->stop_flag!=0)return0;如果(!uv__has_active_handles(loop)&&!uv__has_active_reqs(loop))返回0;如果(!QUEUE_EMPTY(&loop->idle_handles))返回0;如果(!QUEUE_EMPTY(&loop->pending_queue))返回0;if(loop->closing_handles)返回0;returnuv__next_timeout(loop);}这是一个多步条件判断函数,我们一一分析:ifevent循环已经(或正在)结束(uv_stop()被调用,stop_flag!=0),timeout为0if没有要处理的异步任务,timeout为0如果有未处理的idle_handles和pending_queue,timeout为0(对于idle_handles和pending_queue代表什么我也不清楚,如果后面有相应的信息会及时更新)如果还有未清理的资源,超时为0。如果不满足以上条件,使用uv__next_timeout处理intuv__next_timeout(constuv_loop_t*loop){conststructheap_node*heap_node;常量uv_timer_t*句柄;uint64_t差异;heap_node=heap_min((conststructheapp*)&loop->timer_heap);如果(heap_node==NULL)返回-1;/*无限期阻塞*/handle=container_of(heap_node,uv_timer_t,heap_node);如果(句柄->超时<=循环->时间)返回0;//这段代码给出了重点指导//比较当前循环的时间戳diff=handle->timeout-loop->time;//不能大于最大INT_MAXif(diff>INT_MAX)diff=INT_MAX;returndiff;}综上所述,当事件循环满足以下条件时,poll阶段会被阻塞:事件循环还没有触发关闭动作,异步队列还没有处理完所有资源,所有资源都已经关闭,并且阻塞时间不长于给定的时间为什么setTimeout和setImmediate的执行顺序不一定在非I/O周期?上面说了setTimeout和setImmediate的执行顺序不一定在非I/O循环中,如:setTimeout(functiontimeout(){console.log('timeout');},0);setImmediate(functionimmediate(){console.log('immediate');});$nodetimeout_vs_immediate.jstimeoutimmediate$nodetimeout_vs_immediate.jsimmediatetimeout同样的代码,两次运行的结果相反,为什么呢?在node中,setTimeout(cb,0)===setTimeout(cb,1)在事件循环的第一阶段(定时器阶段),node会从一堆定时器中取出一个阈值最小的定时器与之通信loop->time进行比较,如果阈值小于或等于loop->time,说明定时器已经超时,会执行相应的回调(后面会检查一个定时器),如果没有则进入下一阶段,所以第一阶段是否执行setTimeout取决于loop->time的大小。这里可能有两种情况:因为第一次循环之前的准备时间超过1ms,当前循环->time>=1,然后uv_run_timer生效,先执行timeout。由于第一次循环之前的准备时间不到1ms,且当前loop->time<1,所以本次循环中第一个uv_run_timer不会生效,那么在io_poll之后先执行uv_run_check,即先执行immediate,然后closecb执行完后,继续执行uv_run_timer。这就是为什么同一段代码的执行结果是随机的。那为什么说在I/O回调中必须先执行立即执行呢?其实很好理解。考虑以下场景://timeout_vs_immediate.jsconstfs=require('fs');fs.readFile(__filename,()=>{setTimeout(()=>{console.log('timeout');},0);setImmediate(()=>{console.log('immediate');});});由于timeout和immediate事件注册是在readFile的回调执行时触发的,所以难免每次uv_run_timer从事件循环进来,readFile的回调执行之前,都不会触发timeout事件。然后在执行readFile时,poll阶段收到监听到的fd事件完成后,执行回调。此时超时事件注册为立即事件注册。由于执行了readFile回调,它会从uv_io_poll中出来。此时立即执行uv_run_check,所以执行immediateevent。最后一个uv_run_timer检查超时事件,执行超时事件,所以你会发现在I/O回调中注册的两个总是先立即执行。JS调用栈展开是什么意思?栈扩展主要是指抛出异常后逐层匹配catch语句的过程。例如:functiona(){b();}functionb(){c();}functionc(){thrownewError('fromfunctionc');}a();本例中c函数抛出异常,首先检查c函数本身是否有try相关的catch语句,如果没有则退出当前函数并释放当前函数的内存并销毁局部object,在b函数中继续查找,这个过程叫做stackunwinding。参考https://zhuanlan.zhihu.com/p/...https://cnodejs.org/topic/57d...http://gngshn.github.io/2017/...http://文档.libuv.org/en/v1.x…
