Node的事件循环

作者: 励志摆脱懒癌的少女酱 | 来源:发表于2018-07-26 12:47 被阅读43次

1.同步任务与异步任务
(1)同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务;
(2)异步任务:不进入主线程而是进入任务队列的任务,只有等主线程的任务执行完毕后,任务队列开始通知主线程,请求将异步任务进入到主线程执行;

2.浏览器环境与node环境的事件循环机制
(1)浏览器环境:在HTML5中定义的规范
  js执行为单线程(不考虑web worker),所有代码皆在执行线程调用栈完成执行;当执行线程任务清空后才会去轮询取任务队列中任务。

  • 任务队列
       浏览器对不同的异步操作,将其添加到任务队列的时机也不同—由浏览器内核的webcore来执行,其包含3种webAPI:
    • DOM Binding:处理DOM绑定事件,若绑定事件触发时,回调函数立即被webcore添加到任务队列中;
    • network:处理ajax请求,在网络请求返回时,才将对应的回调函数添加到队列中;
    • timer:对setTimeout等计时器进行延时处理,当时间到达时才会将回调函数添加到任务队列中;
  • 异步任务类别及执行顺序
    • macrotask(宏任务—task):script中代码、setTimeout、setInterval、I/O、UI render。
    • microtask(微任务): promise、Object.observe、MutationObserver。


      浏览器异步任务执行顺序
      • 具体过程
        (1)执行完主执行线程中的任务(初始执行线程中没有代码,每一个script标签中的代码是一个独立的macrotask)。
        (2)取出Microtask Queue中任务执行直到清空(若microtask一直被添加,则会继续执行microtask,卡死macrotask)。
        (3)取出Macrotask Queue中一个任务执行。
        (4)取出Microtask Queue中任务执行直到清空。
        (5)重复(3)和(4)

(2)node环境:由libuv库实现;
  node基于事件循环实现非阻塞和事件驱动,其事件循环按阶段执行;

Node中的事件循环阶段:每个阶段都有对应的任务队列,一次tick就是完成所有阶段的一次执行
  • 阶段详情
    (1)timers(定时器阶段):处理setTimeout()和setInterval()设定的回调函数队列;

     一个timer事件指定一个下限时间而不是准确的时间,在达到这个下限时间后+主线程空闲时,执行该事件对应的回调函数,从技术上来说,poll阶段控制timers什么时候执行,而执行的具体位置在timers(poll阶段会控制是否进入下个timers阶段);
    

    (2)I/O callbacks阶段:执行一些系统操作的回调(比如网络通信的错误回调);

    (3)idle、prepare:仅供libuv内部调用;

    (4)poll(轮询阶段): 等待还未返回的I/O事件,任何异步方法(除timers、setImmediate、close外)完成时,都会将其加到poll queue里,并立即执行;

    • 主要功能:

      (i) 处理poll队列里的事件;
      (ii)执行下限时间已经达到的timers的回调(进入下一个事件循环) ;

    • 当事件循环进入poll阶段:
      (i)poll队列不为空的时候,事件循环肯定是先遍历队列并同步执行回调,直到队列清空或执行回调数达到系统上限。
      (ii)poll队列为空的时候,这里有两种情况。
      1)如果代码已经被setImmediate()设定了回调,那么事件循环直接结束poll阶段进入check阶段来执行check队列里的回调。
      2)如果代码没有被设定setImmediate()设定回调:
      * 如果有被设定的timers,那么此时事件循环会检查timers,如果有一个或多个timers下限时间已经到达,那么事件循环将绕回timers阶段,并执行timers的有效回调队列(进入下一个事件循环阶段了)。
      * 如果没有被设定timers,这个时候事件循环是阻塞在poll阶段等待回调被加入poll队列。

    (5)check阶段:执行setImmediate()设定的回调;
    * setImmediate()实际上是一个特殊的timer,跑在事件循环中的一个独立的阶段;它使用libuv的API来设定在:
    * poll阶段结束后立即执行回调;
    * poll阶段空闲时,不让阻塞在poll阶段直接跳到check阶段执行回调。

    (6)close callbacks阶段:如果一个socket或handle被突然关掉(比如socket.destroy()),close事件将在这个阶段被触发,否则将通过process.nextTick()触发。

  • 任务队列类型
    原生的libuv事件循环处理的队列有4种主要类型:
    (1)Timers Queue;
    (2)I/O Queue;
    (3)Check Queue;
    (4)Close Queue;
    中间队列有2种:
    (1)Next tick队列:process.nextTick()
    (2)Other Microtasks:包括其他 microtask,如 resolved promise回调;
      ** Next tick队列比Other Microtasks队列具有更高的优先级**;不过,它们都在事件循环的两个阶段之间进行处理,也就是在结束一个阶段后libuv通信回传到上层;

    注:NodeJS中不同类型的事件在自己的队列中排队;中间队列是只要一个阶段完成,事件循环就会检查这两个中间队列是否有可执行的任务,若有则立即处理它们直到为空,一旦为空,事件循环将继续到下一个阶段。

    一次tick的流程
    • 具体过程
      (1)清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
      (2)清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
      (3)清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
      (4)清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
      (5)进入下轮循环(tick);

4.代码

function sleep(time) {
  let startTime = new Date()
  while (new Date() - startTime < time) {}
  console.log('1s over')
}
setTimeout(() => {
  console.log('setTimeout - 1')
  setTimeout(() => {
      console.log('setTimeout - 1 - 1')
      sleep(1000)
  })
  new Promise(resolve => resolve()).then(() => {
      console.log('setTimeout - 1 - then')
      new Promise(resolve => resolve()).then(() => {
          console.log('setTimeout - 1 - then - then')
      })
  })
  sleep(1000)
})

setTimeout(() => {
  console.log('setTimeout - 2')
  setTimeout(() => {
      console.log('setTimeout - 2 - 1')
      sleep(1000)
  })
  new Promise(resolve => resolve()).then(() => {
      console.log('setTimeout - 2 - then')
      new Promise(resolve => resolve()).then(() => {
          console.log('setTimeout - 2 - then - then')
      })
  })
  sleep(1000)
})
浏览器输出
node输出

6.Node的异步I/O模型
(1)基本要素:事件循环、观察者、请求对象、IO线程池;

  • 事件循环:典型的生产者/消费者模型;

    • 事件的产生:网络请求、文件IO等操作;
    • 事件的消费:主线程空闲时从观察者那儿取出事件并处理其回调;
  • 观察者:在每个Tick的过程中,通过观察者判断是否有事件需要处理;

      小剧场:
      * 主线程:饭馆的厨房;
      * 观察者:收银台的小妹;
      * 事件及回调函数:客人的点单;
      * 剧情:厨房一轮一轮炒菜,但是具体要炒什么菜取决于收银台收到的客人的下单。
    
  • 请求对象: 从js发起回调到内核执行完IO操作的过渡过程中的中间产物

      以fs.open(path,flags,[mode],callback)打开某个文件为例:从js调用Node的核心模块->核心模块调用C++内建模块->内建模块调用libuv进行系统调用:uv_fs_open():
      (1)创建一个FSReqWrap请求对象:封装js层传入的参数和open()方法,将回调函数设置到该对象的oncomplete_sym属性上;
      (2)将这个请求对象推入线程池中等待执行:当线程池有可用线程时,调用相应的底层函数:fs_open();
       js调用完后立即返回,js线程可以继续执行当前任务的后续操作,当前的I/O操作在线程池中等待操作,不影响js线程。
    
fs.open()流程图
  • 执行回调:以windows平台为例
    • 线程池中的I/O操作调用完后,会将获取的结果存储到req->result属性上,调用PostQueuedCompletionStatus()向IOCP(IO完成端口)提交执行状态,告知当前对象操作已经完成,并将线程归还线程池
    • I/O观察者在每次Tick的执行中,调用GetQueuedCompletionStatus()检查线程池是否有执行完的请求,若存在,将请求对象加入到I/O观察者队列中,然后将其当作事件处理:取出请求对象的result属性做参数,取出oncomplete_sym属性做方法,然后调用执行。

(2)基本流程

  • 第一阶段:组装好对象 -> 送入IO线程池等待执行;
  • 第二阶段:回调通知;


    node中整个异步IO的流程

[参考文献]
1.NodeJS事件循环(英文版/中文版)
2.node中的Event模块
3.浏览器和Node不同的事件循环(Event Loop)
4.《深入浅出nodejs》

相关文章

  • node 事件

    1、事件 1.1普通事件的使用 1.2、Node.js 的事件循环机制解析 1)Node 由事件循环开始,到事件循...

  • node 事件循环

    概念 -单线程、单进程,结合V8的异步回调接口,处理大量并发-API支持回调函数-事件机制采用设计模式中观察者模式...

  • Node事件循环

    Node.js 事件循环机制 Node.js 采用事件驱动和异步 I/O 的方式,实现了一个单线程、高并发的 Ja...

  • node事件循环

    事件循环 事件循环是一个典型的生产者/消费者模式,网络请求,异步IO源源不断的产生提供不同类型的事件到观察者哪里,...

  • node事件循环

    浏览器事件循环见:https://www.jianshu.com/p/64bbefbe5ae5[https://w...

  • Node事件循环

    Node架构图 事件循环核心 核心模块就是LIBUV 在linux上,libuv是对epoll的封装; 在wind...

  • Node事件循环

    官网事件循环:https://nodejs.org/zh-cn/docs/guides/event-loop-ti...

  • 学习 nodejs I /O 交互

    1 事件循环 Node的执行模型实际上是事件循环。在进程启动时,Node会创建一个无限循环,每一次执行循环体的过程...

  • node.js的事件循环

    在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现...

  • Node的事件循环

    1.同步任务与异步任务(1)同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行下一个任务;(2)...

网友评论

    本文标题:Node的事件循环

    本文链接:https://www.haomeiwen.com/subject/ygfbmftx.html