精读Javascript系列(八)事件循环细则 II: NodeJS事件循环相关

前言

本篇也是过渡篇,主要补充NodeJS的重点和难点。

嘛,废话不多说,正文开始。

NodeJS

首先,理解 NodeJS 有一个很重要的前提,就是知道它究竟是 单线程的,还是 多线程 的?

还是如以往一样,越是简单的问题答案就每个人的答案就越是令人迷惑;
实践是检验真理的唯一标准,最后看看 node线程数就知道了——7个,所以它是多线程的。
但是为何有人说它是 单线程 呢?

单线程这个说法应该被严重曲解的,导致出现了一些误导性; 完整说法应该是 NodeJS运行Javascript代码只有一个线程, 这个特征与浏览器如出一辙,浏览器运行Javascript也是单线程的。然而却不能说浏览器就是单线程,同样的NodeJS也是这个道理。

所以从根本上来说,NodeJS只有一个运行JS的主线程,它也是负责事件循环的线程; 与其他异步模型一般,它永远不会阻塞。 Node官网的事件循环,太过简洁了在细节方面做得不够详尽,因此只能参考实现它的组件——libuv但这个细节也太多了)。

NodeJS 的事件循环其实是由 libuv 实现的,所以只有理解了 libuv 的事件循环才能算彻底理解了 nodejs 的事件循环。libuv 实现机制要细说三天三夜也讲不完, 所以长话短说——

  1. libuv 负责收集所有事件(它也可能会来自外部、也包括内核的)
  2. 针对这些事件注册回调函数
  3. 事件发生后被执行回调函数。

其实原理与浏览器环境大同小异,都是采用消息传递的异步通信方式,并且都是基于事件循环来执行代码的。不过要注意的是Node 程序处理最多的不是 页面渲染, 而是 输入和输出,统称作IO, 例如文件读写、网络请求等等,这类操作统一特点都是阻塞(响应时间超长)的,传统的读写事件都是同步的,也就是说在获取文件全部内容之前,根本无法做任何事情,自然也效率低下,我们的任务就是狠狠的压榨CPU性能

libuv便是上面问题的一个解决方案,基本思路是将一些阻塞任务(主要是IO,网络请求.etc)这类阻塞任务分配到工作线程中等待稍后执行,这些工作线程会统一保存在线程池中;然后开启事件循环一一处理。 至于一次能处理多少事件(可能一次事件循环爆发式接收到500个请求),取决于计算机硬件。

所以,理解 nodejs 事件循环, 还是要从 libuv事件循环开始。
下面是官网给出的事件循环示意图:
libuv 概览图:
在这里插入图片描述

理解事件循环难点:

  • 在主模块代码结束后,事件注册才会完成。一旦有注册的事件,就必然会产生一个handler,它可以标识任务的上下文(术语:事件句柄、文件描述符)但这时还没有正式开启事件循环
  • 当有handler存在,初始化线程池。然后将已经注册事件的handler分配到线程池的对应workthread中。例如setTimeouthandlers(Timer)会分配到TimersetImmediatehandler(CheckHandler)分配到Check, 其他事件分配到poll中统一处理。
  • 开启事件循环,然后更新事件循环的handler计数 ; 现在能够看出,事件循环的每个phase其实都保存在了线程池中。
  • 也正因为如此,phase并不是严守顺序的;或是说它是跳跃性的。例如当第一次进行事件循环时,Timer没有到期,所以不会执行回调。但反过来说一旦到期就一定会执行回调。由于没有需要异步完成的IO Callback,所以不会进入pending callback
  • 因此,首次事件循环会直接跳跃到poll阶段完成IO Callbackpoll阶段极其特殊,它有一个阻塞时长,如果Callback在阻塞时长内没有完成,相应的io callback异步完成,即在下一个事件循环的pending callback处理完所有剩余的callback。同样的道理,如果在阻塞时长内存在setImmediate的回调,那么就会立即进入到check阶段(不用在意几回合事件循环)。
  • 每个事件循环跳跃下一个事件循环前,都会有一个Close Handler过程,它会清理掉完成状态(done=true)的handler,更新事件循环的引用计数等等。

事件循环的核心,poll阶段的总结就是(有点都合主义的味道):

  1. 计算Poll阶段的阻塞时间(即Timeout)
  2. 执行已发生事件的回调函数,但还有:
    1. 是否有nextTickmicrotasks? 如果有则执行,否则继续
    2. 检查是否有setImmediate回调,如果有执行回调然后进入Check阶段。
    3. 若无,继续。
  3. 在阻塞时间内等待事件发生,如果存在Timer,那么阻塞时间是最近的Timer到期时间,否则不限制阻塞时间
    1. 若有事件发生,立即执行相应的回调函数,此步骤完全等价于(2)
    2. 若无则继续等待下去(直至node 停止监听、或没有活跃的handler)。
    3. 如果在阻塞时间内仍有未回调完成的任务handler),挂起在下一个事件循环的pending callback阶段排队处理。
  4. 超过阻塞时间后,进入下一个事件循环,执行到期Timer
    1. 此时不再有可用setImmediate回调,因此前移
    2. 阻塞时间是最近的Timer到期时间

特别注意

  • (2)实际上是(3)其中的一个步骤,但是分离出来更容易理解一些。
  • 虽然官网说是绕回,但是我觉得这里应该是前移到下一个事件循环中更准确一点。这是和nodejs官网矛盾点。
    想要了解libuv事件循环所有细节的,可以直接跳到后面。

setTimeout VS setImmediate

这是很令人兴奋的话题,它们到底谁更优先? 事实上它们并没有优先级关系,因为事件循环没办法用顺序去理解,因此不要用谁优先谁就执行原则,而是谁触发谁就必须执行原则。

注意两点:

  1. setTimeout是到期立即执行
  2. setImmediate是进入到check阶段后执行。

也就是说,TimerCheckHandler是没有逻辑上的必然关系的。

例如:

setTimeout(()=>{
   
    var start = Date.now();
    setTimeout(()=>{
   
        console.log('------------------');
        console.log('timeout 1 = '+(Date.now(
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值