• 技术文章 >web前端 >js教程

    Nodejs进阶学习:深入了解异步I/O和事件循环

    青灯夜游青灯夜游2021-09-23 21:49:40转载270
    本篇文章是Nodejs的进阶学习,带大家详细了解一下Nodejs中的异步I/O和事件循环,希望对大家有所帮助!

    本文讲详细讲解 nodejs 中两个比较难以理解的部分异步I/O事件循环,对 nodejs 核心知识点,做梳理和补充。【推荐学习:《nodejs 教程》】

    送人玫瑰,手有余香,希望阅读后感觉不错的同学,可以给点个赞,鼓励我继续创作前端硬文。

    老规矩我们带上疑问开始今天的分析:

    异步I/O

    概念

    处理器访问任何寄存器和 Cache 等封装以外的数据资源都可以当成 I/O 操作,包括内存,磁盘,显卡等外部设备。在 Nodejs 中像开发者调用 fs 读取本地文件或网络请求等操作都属于I/O操作。(最普遍抽象 I/O 是文件操作和 TCP/UDP 网络操作)

    Nodejs 为单线程的,在单线程模式下,任务都是顺序执行的,但是前面的任务如果用时过长,那么势必会影响到后续任务的进行,通常 I/O 与 cpu 之间的计算是可以并行进行的,但是同步的模式下,I/O的进行会导致后续任务的等待,这样阻塞了任务的执行,也造成了资源不能很好的利用。

    为了解决如上的问题,Nodejs 选择了异步I/O的模式,让单线程不再阻塞,更合理的使用资源。

    如何合理的看待Nodejs中异步I/O

    前端开发者可能更清晰浏览器环境下的 JS 的异步任务,比如发起一次 ajax 请求,正如 ajax 是浏览器提供给 js 执行环境下可以调用的 api 一样 ,在 Nodejs 中提供了 http 模块可以让 js 做相同的事。比如监听|发送 http 请求,除了 http 之外,nodejs 还有操作本地文件的 fs 文件系统等。

    如上 fs http 这些任务在 nodejs 中叫做 I/O 任务。理解了 I/O 任务之后,来分析一下在 Nodejs 中,I/O 任务的两种形态——阻塞和非阻塞。

    nodejs中同步和异步IO模式

    nodejs 对于大部分的 I/O 操作都提供了阻塞非阻塞两种用法。阻塞指的是执行 I/O 操作的时候必须等待结果,才往下执行 js 代码。如下一下阻塞代码

    同步I/O模式

    /* TODO:  阻塞 */
    const fs = require('fs');
    const data = fs.readFileSync('./file.js');
    console.log(data)
    /* TODO: 阻塞 - 捕获异常  */
    try{
        const fs = require('fs');
        const data = fs.readFileSync('./file1.js');
        console.log(data)
    }catch(e){
        console.log('发生错误:',e)
    }
    console.log('正常执行')

    同步 I/O 模式造成代码执行等待 I/O 结果,浪费等待时间,CPU 的处理能力得不到充分利用,I/O 失败还会让整整个线程退出。阻塞 I / O 在整个调用栈上示意图如下:

    1.png

    异步I/O模式

    这就是刚刚介绍的异步I/O。首先看一下异步模式下的 I/O 操作:

    /* TODO: 非阻塞 - 异步 I/O */
    const fs = require('fs')
    fs.readFile('./file.js',(err,data)=>{
        console.log(err,data) // null  <Buffer 63 6f 6e 73 6f 6c 65 2e 6c 6f 67 28 27 68 65 6c 6c 6f 2c 77 6f 72 6c 64 27 29>
    })
    console.log(111) // 111 先被打印~
    
    fs.readFile('./file1.js',(err,data)=>{
        console.log(err,data) // 保存  [ no such file or directory, open './file1.js'] ,找不到文件。
    })

    比如如上的 callback ,作为一个异步回调函数,就像 setTimeout(fn) 的 fn 一样,不会阻塞代码执行。会在得到结果后触发,对于 Nodejs 异步执行 I/O 回调的细节,接下来会慢慢剖析。

    对于异步 I/O 的处理, Nodejs 内部使用了线程池来处理异步 I/O 任务,线程池中会有多个 I/O 线程来同时处理异步的 I/O 操作,比如如上的的例子中,在整个 I/O 模型中会这样。

    2.png

    接下来将一起探索一下异步 I/O 执行过程。

    事件循环

    和浏览器一样,Nodejs 也有自身的执行模型——事件循环( eventLoop ),事件循环的执行模型受到宿主环境的影响,它不属于 javascript 执行引擎( 例如 v8 )的一部分,这就导致了不同宿主环境下事件循环模式和机制可能不同,直观的体现就是 Nodejs 和浏览器环境下对微任务( microtask )和宏任务( macrotask )处理存在差异。对于 Nodejs 的事件循环及其每一个阶段,接下来会详细探讨。

    Nodejs 的事件循环有多个阶段,其中有一个专门处理 I/O 回调的阶段,每一个执行阶段我们可以称之为 Tick , 每一个 Tick 都会查询是否还有事件以及关联的回调函数 ,如上异步 I/O 的回调函数,会在 I/O 处理阶段检查当前 I/O 是否完成,如果完成,那么执行对应的 I/O 回调函数,那么这个检查 I/O 是否完成的观察者我们称之为 I/O 观察者。

    观察者

    如上提到了 I/O 观察者的概念,也讲了 Nodejs 中会有多个阶段,事实上每一个阶段都有一个或者多个对应的观察者,它们的工作很明确就是在每一次对应的 Tick 过程中,对应的观察者查找有没有对应的事件执行,如果有,那么取出来执行。

    浏览器的事件来源于用户的交互和一些网络请求比如 ajax 等, Nodejs 中,事件来源于网络请求 http ,文件 I/O 等,这些事件都有对应的观察者,我这里枚举出一些重要的观察者。

    在 Nodejs 中,对应观察者接收对应类型的事件,事件循环过程中,会向这些观察者询问有没有该执行的任务,如果有,那么观察者会取出任务,交给事件循环去执行。

    请求对象与线程池

    JavaScript 调用到计算机系统执行完 I/O 回调,请求对象充当着很重要的作用,我们还是以一次异步 I/O 操作为例

    请求对象: 比如之前调用 fs.readFile ,本质上调用 libuv 上的方法创建一个请求对象。这个请求对象上保留着此次 I/O 请求的信息,包括此次 I/O 的主体和回调函数等。然后异步调用的第一阶段就完成了,JavaScript 会继续往下执行执行栈上的代码逻辑,当前的 I/O 操作将以请求对象的形式放入到线程池中,等待执行。达到了异步 I/O 的目的。

    线程池: Nodejs 的线程池在 Windows 下有内核( IOCP )提供,在 Unix 系统中由 libuv 自行实现, 线程池用来执行部分的 I/O (系统文件的操作),线程池大小默认为 4 ,多个文件系统操作的请求可能阻塞到一个线程中。那么线程池里面的 I/O 操作是怎么执行的呢? 上一步说到,一次异步 I/O 会把请求对象放在线程池中,首先会判断当前线程池是否有可用的线程,如果线程可用,那么会执行请求对象的 I/O 操作,并把执行后的结果返回给请求对象。在事件循环中的 I/O 处理阶段,I/O 观察者会获取到已经完成的 I/O 对象,然后取出回调函数和结果调用执行。I/O 回调函数就这样执行,而且在回调函数的参数重获取到结果。

    异步 I/O 操作机制

    上述讲了整个异步 I/O 的执行流程,从一个异步 I/O 的触发,到 I/O 回调到执行。事件循环观察者请求对象线程池 构成了整个异步 I/O 执行模型。

    用一幅图表示四者的关系:

    3.png

    总结上述过程:

    事件循环

    事件循环机制由宿主环境实现

    上述中已经提及了事件循环不是 JavaScript 引擎的一部分 ,事件循环机制由宿主环境实现,所以不同宿主环境下事件循环不同 ,不同宿主环境指的是浏览器环境还是 nodejs 环境 ,但在不同操作系统中,nodejs 的宿主环境也是不同的,接下来用一幅图描述一下 Nodejs 中的事件循环和 javascript 引擎之间的关系。

    以 libuv 下 nodejs 的事件循环为参考,关系如下:

    4.png

    以浏览器下 javaScript 的事件循环为参考,关系如下:

    5.png

    事件循环本质上就像一个 while 循环,如下所示,我来用一段代码模拟事件循环的执行流程。

    const queue = [ ... ]   // queue 里面放着待处理事件
    while(true){
        //开始循环
        //执行 queue 中的任务
        //....
    
        if(queue.length ===0){
           return // 退出进程
        }
    }

    我总结了流程图如下所示:

    6.png

    那么如何事件循环是如何处理这些任务的呢?我们列出 Nodejs 中一些常用的事件任务:

    接下来会一一讲到 ,这些任务的原理以及 nodejs 是如何处理这些任务的。

    1 事件循环阶段

    对于不同的事件任务,会在不同的事件循环阶段执行。根据 nodejs 官方文档,在通常情况下,nodejs 中的事件循环根据不同的操作系统可能存在特殊的阶段,但总体是可以分为以下 6 个阶段 (代码块的六个阶段) :

    /*
       ┌───────────────────────────┐
    ┌─>│           timers          │     -> 定时器,延时器的执行    
    │  └─────────────┬─────────────┘
    │  ┌─────────────┴─────────────┐
    │  │     pending callbacks     │     -> i/o
    │  └─────────────┬─────────────┘
    │  ┌─────────────┴─────────────┐
    │  │       idle, prepare       │
    │  └─────────────┬─────────────┘      ┌───────────────┐
    │  ┌─────────────┴─────────────┐      │   incoming:   │
    │  │           poll            │<─────┤  connections, │
    │  └─────────────┬─────────────┘      │   data, etc.  │
    │  ┌─────────────┴─────────────┐      └───────────────┘
    │  │           check           │
    │  └─────────────┬─────────────┘
    │  ┌─────────────┴─────────────┐
    └──┤      close callbacks      │
       └───────────────────────────┘
    */

    对于每一个阶段的执行特点和对应的事件任务,我接下来会详细剖析。我们看一下六个阶段在底层源码中是怎么样体现的。

    我们看一下 libuv 下 nodejs 的事件循环的源代码(在 unixwin 有点差别,不过不影响流程,这里以 unix 为例子。):

    libuv/src/unix/core.c

    int uv_run(uv_loop_t* loop, uv_run_mode mode) {
      // 省去之前的流程。
      while (r != 0 && loop->stop_flag == 0) {
    
        /* 更新事件循环的时间 */ 
        uv__update_time(loop);
    
        /*第一阶段: timer 阶段执行  */
        uv__run_timers(loop);
    
        /*第二阶段: pending 阶段 */
        ran_pending = uv__run_pending(loop);
    
        /*第三阶段: idle prepare 阶段 */
        uv__run_idle(loop);
        uv__run_prepare(loop);
    
        timeout = 0;
        if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
         /* 计算 timeout 时间  */
          timeout = uv_backend_timeout(loop);
        
        /* 第四阶段:poll 阶段 */
        uv__io_poll(loop, timeout);
    
        /* 第五阶段:check 阶段 */
        uv__run_check(loop);
        /* 第六阶段: close 阶段  */
        uv__run_closing_handles(loop);
        /* 判断当前线程还有任务 */ 
         r = uv__loop_alive(loop);
    
        /* 省去之后的流程 */
      }
      return r;
    }

    2 任务队列

    在整个事件循环过程中,有四个队列(实际的数据结构不是队列)是在 libuv 的事件循环中进行的,还有两个队列是在 nodejs 中执行的分别是 promise 队列nextTick 队列。

    在 NodeJS 中不止一个队列,不同类型的事件在它们自己的队列中入队。在处理完一个阶段后,移向下一个阶段之前,事件循环将会处理两个中间队列,直到两个中间队列为空。

    libuv 处理任务队列

    事件循环的每一个阶段,都会执行对应任务队列里面的内容。

    非 libuv 中间队列

    中间队列的执行特点:

    /* TODO: 打印顺序  */
    setTimeout(()=>{
        console.log('setTimeout 执行')
    },0)
    
    const p = new Promise((resolve)=>{
         console.log('Promise执行')
         resolve()
    })
    p.then(()=>{
        console.log('Promise 回调执行')
    })
    
    process.nextTick(()=>{
        console.log('nextTick 执行')
    })
    console.log('代码执行完毕')

    如上代码块中的 nodejs 中的执行顺序是什么?

    效果:

    7.png

    打印结果:Promise执行 -> 代码执行完毕 -> nextTick 执行 -> Promise 回调执行 -> setTimeout 执行

    解释:很好理解为什么这么打印,在主代码事件循环中, Promise执行代码执行完毕 最先被打印,nextTick 被放入 nextTick 队列中,Promise 回调放入 Microtasks 队列中,setTimeout 被放入 timer 堆中。接下来主循环完成,开始清空两个队列中的内容,首先清空 nextTick 队列,nextTick 执行 被打印,接下来清空 Microtasks 队列,Promise 回调执行 被打印,最后再判断事件循环 loop 中还有 timer 任务,那么开启新的事件循环 ,首先执行,timer 任务,setTimeout 执行被打印。 整个流程完毕。

    /* TODO: 阻塞 I/O 情况 */
    process.nextTick(()=>{
        const now = +new Date()
        /* 阻塞代码三秒钟 */
        while( +new Date() < now + 3000 ){}
    })
    
    fs.readFile('./file.js',()=>{
        console.log('I/O: file ')
    })
    
    setTimeout(() => {
        console.log('setTimeout: ')
    }, 0);

    效果:

    8.gif

    3 事件循环流程图

    接下来用流程图,表示事件循环的六大阶段的执行顺序,以及两个优先队列的执行逻辑。

    9.png

    4 timer 阶段 -> 计时器 timer / 延时器 interval

    延时器计时器观察者(Expired timers and intervals):延时器计时器观察者用来检查通过 setTimeoutsetInterval创建的异步任务,内部原理和异步 I/O 相似,不过定期器/延时器内部实现没有用线程池。通过setTimeoutsetInterval定时器对象会被插入到延时器计时器观察者内部的二叉最小堆中,每次事件循环过程中,会从二叉最小堆顶部取出计时器对象,判断 timer/interval 是否过期,如果有,然后调用它,出队。再检查当前队列的第一个,直到没有过期的,移到下一个阶段。

    libuv 层如何处理 timer

    首先一起看一下 libuv 层是如何处理的 timer

    libuv/src/timer.c

    void uv__run_timers(uv_loop_t* loop) {
      struct heap_node* heap_node;
      uv_timer_t* handle;
    
      for (;;) {
        /* 找到 loop 中 timer_heap 中的根节点 ( 值最小 ) */  
        heap_node = heap_min((struct heap*) &loop->timer_heap);
        /*  */
        if (heap_node == NULL)
          break;
    
        handle = container_of(heap_node, uv_timer_t, heap_node);
        if (handle->timeout > loop->time)
          /*  执行时间大于事件循环事件,那么不需要在此次 loop 中执行  */
          break;
    
        uv_timer_stop(handle);
        uv_timer_again(handle);
        handle->timer_cb(handle);
      }
    }

    如上是 timer 阶段在 libuv 中执行特点。接下里分析一下 node 中是如何处理定时器延时器的。

    node 层如何处理 timer

    在 Nodejs 中 setTimeoutsetInterval 是 nodejs 自己实现的,来一起看一下实现细节:

    node/lib/timers.js

    function setTimeout(callback,after){
        //...
        /* 判断参数逻辑 */
        //..
        /* 创建一个 timer 观察者 */
        const timeout = new Timeout(callback, after, args, false, true);
        /* 将 timer 观察者插入到 timer 堆中  */
        insert(timeout, timeout._idleTimeout);
    
        return timeout;
    }

    那么 Timeout 做了些什么呢?

    node/lib/internal/timers.js

    function Timeout(callback, after, args, isRepeat, isRefed) {
      after *= 1 
      if (!(after >= 1 && after <= 2 ** 31 - 1)) {
        after = 1 // 如果延时器 timeout 为 0 ,或者是大于 2 ** 31 - 1 ,那么设置成 1 
      }
      this._idleTimeout = after; // 延时时间 
      this._idlePrev = this;
      this._idleNext = this;
      this._idleStart = null;
      this._onTimeout = null;
      this._onTimeout = callback; // 回调函数
      this._timerArgs = args;
      this._repeat = isRepeat ? after : null;
      this._destroyed = false;  
    
      initAsyncResource(this, 'Timeout');
    }

    timer 处理流程图

    用一副流程图描述一下,我们创建一个 timer ,再到 timer 在事件循环里面执行的流程。

    10.png

    timer 特性

    这里有两点需要注意:

    验证结论一次执行一个 timer 任务 ,先来看一段代码片段:

    setTimeout(()=>{
        console.log('setTimeout1:')
        process.nextTick(()=>{
            console.log('nextTick')
        })
    },0)
    setTimeout(()=>{
        console.log('setTimeout2:')
    },0)

    打印结果:

    11.png

    nextTick 队列是在事件循环的每一阶段结束执行的,两个延时器的阀值都是 0 ,如果在 timer 阶段一次性执行完,过期任务的话,那么打印 setTimeout1 -> setTimeout2 -> nextTick ,实际上先执行一个 timer 任务,然后执行 nextTick 任务,最后再执行下一个 timer 任务。

    5 pending 阶段

    pending 阶段用来处理此次事件循环之前延时的 I/O 回调函数。首先看一下在 libuv 中执行时机。

    libuv/src/unix/core.c

    static int uv__run_pending(uv_loop_t* loop) {
      QUEUE* q;
      QUEUE pq;
      uv__io_t* w
      /* pending_queue 为空,清空队列 ,返回 0  */
      if (QUEUE_EMPTY(&loop->pending_queue))
        return 0;
      
      QUEUE_MOVE(&loop->pending_queue, &pq);
      while (!QUEUE_EMPTY(&pq)) { /* pending_queue 不为空的情况,清空 I/O 回调。返回 1  */
        q = QUEUE_HEAD(&pq);
        QUEUE_REMOVE(q);
        QUEUE_INIT(q);
        w = QUEUE_DATA(q, uv__io_t, pending_queue);
        w->cb(loop, w, POLLOUT);
      }
      return 1;
    }

    6 idle, prepare 阶段

    idle 做一些 libuv 一些内部操作, prepare 为接下来的 I/O 轮询做一些准备工作。接下来一起解析一下比较重要 poll 阶段。

    7 poll I / O 轮询阶段

    在正式讲解 poll 阶段做哪些事情之前,首先看一下,在 libuv 中,轮询阶段的执行逻辑:

      timeout = 0;
        if ((mode == UV_RUN_ONCE && !ran_pending) || mode == UV_RUN_DEFAULT)
          /* 计算 timeout   */
          timeout = uv_backend_timeout(loop);
          /* 进入 I/O 轮询 */
          uv__io_poll(loop, timeout);

    timeout代表什么

    首先要明白不同 timeout ,在 I/O 轮询中代表什么意思。

    获取timeout

    timeout 的获取是通过 uv_backend_timeout 那么如何获得的呢?

    int uv_backend_timeout(const uv_loop_t* loop) {
        /* 当前事件循环任务停止 ,不阻塞 */
      if (loop->stop_flag != 0)
        return 0;
       /* 当前事件循环 loop 不活跃的时候 ,不阻塞 */
      if (!uv__has_active_handles(loop) && !uv__has_active_reqs(loop))
        return 0;
      /* 当 idle 句柄队列不为空时,返回 0,即不阻塞。 */
      if (!QUEUE_EMPTY(&loop->idle_handles))
        return 0;
       /* i/o pending 队列不为空的时候。 */  
      if (!QUEUE_EMPTY(&loop->pending_queue))
        return 0;
       /* 有关闭回调 */
      if (loop->closing_handles)
        return 0;
      /* 计算有没有延时最小的延时器 | 定时器 */
      return uv__next_timeout(loop);
    }

    uv_backend_timeout 主要做的事情是:

    接下来看一下 uv__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;
      /* 找到延时时间最小的 timer  */
      heap_node = heap_min((const struct heap*) &loop->timer_heap);
      if (heap_node == NULL) /* 如何没有 timer,那么返回 -1 ,一直进入 poll 状态  */
        return -1; 
    
      handle = container_of(heap_node, uv_timer_t, heap_node);
       /* 有过期的 timer 任务,那么返回 0,poll 阶段不阻塞 */
      if (handle->timeout <= loop->time)
        return 0;
      /* 返回当前最小阀值的 timer 与 当前事件循环的事件相减,得出来的时间,可以证明 poll 可以停留多长时间 */ 
      diff = handle->timeout - loop->time;
      return (int) diff;
    }

    uv__next_timeout 做的事情如下:

    执行io_poll

    接下来就是 uv__io_poll 真正的执行,里面有一个 epoll_wait 方法,根据 timeout ,来轮询有没有 I/O 完成,有得话那么执行 I/O 回调。这也是 unix 下异步I/O 实现的重要环节。

    poll阶段本质

    接下来总结一下 poll 阶段的本质:

    poll 阶段流程图

    我把整个 poll 阶段做的事用流程图表示,省去了一些细枝末节。

    12.png

    8 check 阶段

    如果 poll 阶段进入 idle 状态并且 setImmediate 函数存在回调函数时,那么 poll 阶段将打破无限制的等待状态,并进入 check 阶段执行 check 阶段的回调函数。

    check 做的事就是处理 setImmediate 回调。,先来看一下 Nodejs 中是怎么定义的 setImmediate

    Nodejs 底层中的 setImmediate

    setImmediate定义

    node/lib/timer.js

    function setImmediate(callback, arg1, arg2, arg3) {
      validateCallback(callback); /* 校验一下回调函数 */
       /* 创建一个 Immediate 类   */
       return new Immediate(callback, args);
    }

    node/lib/internal/timers.js

    class Immediate{
       constructor(callback, args) {
        this._idleNext = null;
        this._idlePrev = null; /* 初始化参数 */
        this._onImmediate = callback;
        this._argv = args;
        this._destroyed = false;
        this[kRefed] = false;
    
        initAsyncResource(this, 'Immediate');
        this.ref();
        immediateInfo[kCount]++;
        
        immediateQueue.append(this); /* 添加 */
      }
    }

    setImmediate执行

    poll 阶段之后,会马上到 check 阶段,执行 immediateQueue 里面的 Immediate。 在每一次事件循环中,会先执行一个setImmediate 回调,然后清空 nextTick 和 Promise 队列的内容。为了验证这个结论,同样和 setTimeout 一样,看一下如下代码块:

    setImmediate(()=>{
        console.log('setImmediate1')
        process.nextTick(()=>{
            console.log('nextTick')
        })
    })
    
    setImmediate(()=>{
        console.log('setImmediate2')
    })

    13.png

    打印 setImmediate1 -> nextTick -> setImmediate2 ,在每一次事件循环中,执行一个 setImmediate ,然后执行清空 nextTick 队列,在下一次事件循环中,执行另外一个 setImmediate2 。

    setImmediate执行流程图

    14.png

    setTimeout & setImmediate

    接下来对比一下 setTimeoutsetImmediate,如果开发者期望延时执行的异步任务,那么接下来对比一下 setTimeout(fn,0)setImmediate(fn) 区别。

    如果 setTimeout 和 setImmediate 在一起,那么谁先执行呢?

    首先写一个 demo:

    setTimeout(()=>{
        console.log('setTimeout')
    },0)
    
    setImmediate(()=>{
        console.log( 'setImmediate' )
    })

    猜测

    先猜测一下,setTimeout 发生 timer 阶段,setImmediate 发生在 check 阶段,timer 阶段早于 check 阶段,那么 setTimeout 优先于 setImmediate 打印。但事实是这样吗?

    实际打印结果

    15.png

    从以上打印结果上看, setTimeoutsetImmediate 执行时机是不确定的,为什么会造成这种情况,上文中讲到即使 setTimeout 第二个参数为 0,在 nodejs 中也会被处理 setTimeout(fn,1)。当主进程的同步代码执行之后,会进入到事件循环阶段,第一次进入 timer 中,此时 settimeout 对应的 timer 的时间阀值为 1,若在前文 uv__run_timer(loop) 中,系统时间调用和时间比较的过程总耗时没有超过 1ms 的话,在 timer 阶段会发现没有过期的计时器,那么当前 timer 就不会执行,接下来到 check 阶段,就会执行 setImmediate 回调,此时的执行顺序是: setImmediate -> setTimeout

    但是如果总耗时超过一毫秒的话,执行顺序就会发生变化,在 timer 阶段,取出过期的 setTimeout 任务执行,然后到 check 阶段,再执行 setImmediate ,此时 setTimeout -> setImmediate

    造成这种情况发生的原因是:timer 的时间检查距当前事件循环 tick 的间隔可能小于 1ms 也可能大于 1ms 的阈值,所以决定了 setTimeout 在第一次事件循环执行与否。

    接下来我用代码阻塞的情况,会大概率造成 setTimeout 一直优先于 setImmediate 执行。

    /* TODO:  setTimeout & setImmediate */
    setImmediate(()=>{
        console.log( 'setImmediate' )
    })
    
    setTimeout(()=>{
        console.log('setTimeout')
    },0)
    /* 用 100000 循环阻塞代码,促使 setTimeout 过期 */
    for(let i=0;i<100000;i++){
    }

    效果:

    16.png

    100000 循环阻塞代码,这样会让 setTimeout 超过时间阀值执行,这样就保证了每次先执行 setTimeout -> setImmediate

    特殊情况:确定顺序一致性。我们看一下特殊的情况。

    const fs = require('fs')
    fs.readFile('./file.js',()=>{
        setImmediate(()=>{
            console.log( 'setImmediate' )
        })
        setTimeout(()=>{
            console.log('setTimeout')
        },0)
    })

    如上情况就会造成,setImmediate 一直优先于 setTimeout 执行,至于为什么,来一起分析一下原因。

    万变不离其宗,只要掌握了如上各个阶段的特性,那么对于不同情况的执行情况,就可以清晰的分辨出来。

    9 close 阶段

    close 阶段用于执行一些关闭的回调函数。执行所有的 close 事件。接下来看一下 close 事件 libuv 的实现。

    libuv/src/unix/core.c

    static void uv__run_closing_handles(uv_loop_t* loop) {
      uv_handle_t* p;
      uv_handle_t* q;
    
      p = loop->closing_handles;
      loop->closing_handles = NULL;
    
      while (p) {
        q = p->next_closing;
        uv__finish_close(p);
        p = q;
      }
    }

    10 Nodejs 事件循环总结

    接下来总结一下 Nodejs 事件循环。

    Nodejs事件循环习题演练

    接下来为了更清楚事件循环流程,这里出两道事件循环的问题。作为实践:

    习题一

    process.nextTick(function(){
        console.log('1');
    });
    process.nextTick(function(){
        console.log('2');
         setImmediate(function(){
            console.log('3');
        });
        process.nextTick(function(){
            console.log('4');
        });
    });
    
    setImmediate(function(){
        console.log('5');
         process.nextTick(function(){
            console.log('6');
        });
        setImmediate(function(){
            console.log('7');
        });
    });
    
    setTimeout(e=>{
        console.log(8);
        new Promise((resolve,reject)=>{
            console.log(8+'promise');
            resolve();
        }).then(e=>{
            console.log(8+'promise+then');
        })
    },0)
    
    setTimeout(e=>{ console.log(9); },0)
    
    setImmediate(function(){
        console.log('10');
        process.nextTick(function(){
            console.log('11');
        });
        process.nextTick(function(){
            console.log('12');
        });
        setImmediate(function(){
            console.log('13');
        });
    });
    
    console.log('14');
     new Promise((resolve,reject)=>{
        console.log(15);
        resolve();
    }).then(e=>{
        console.log(16);
    })

    如果刚看这个 demo 可以会发蒙,不过上述讲到了整个事件循环,再来看这个问题就很轻松了,下面来分析一下整体流程:

    最先打印:

    打印console.log('14');

    打印console.log(15);

    nextTick 队列:

    nextTick -> console.log(1) nextTick -> console.log(2) -> setImmediate(3) -> nextTick(4)

    Promise队列

    Promise.then(16)

    check队列

    setImmediate(5) -> nextTick(6) -> setImmediate(7) setImmediate(10) -> nextTick(11) -> nextTick(12) -> setImmediate(13)

    timer队列

    setTimeout(8) -> promise(8+'promise') -> promise.then(8+'promise+then') setTimeout(9)

    清空 nextTick ,打印:

    console.log('1');

    console.log('2');

    执行第二个 nextTick 的时候,又有一个 nextTick ,所以会把这个 nextTick 也加入到队列中。接下来马上执行。

    console.log('4')

    接下来清空Microtasks

    console.log(16);

    此时的 check 队列加入了新的 setImmediate。

    check队列setImmediate(5) -> nextTick(6) -> setImmediate(7) setImmediate(10) -> nextTick(11) -> nextTick(12) -> setImmediate(13) setImmediate(3)

    执行第一个 timer:

    console.log(8);

    此时发现一个 Promise 。在正常的执行上下文中:

    console.log(8+'promise');

    然后将 Promise.then 加入到 nextTick 队列中。接下里会马上清空 nextTick 队列。

    console.log(8+'promise+then');

    执行第二个 timer:

    console.log(9)

    执行第一个 check:

    console.log(5);

    此时发现一个 nextTick ,然后还有一个 setImmediate 将 setImmediate 加入到 check 队列中。然后执行 nextTick 。

    console.log(6)

    执行第二个 check

    console.log(10)

    此时发现两个 nextTick 和一个 setImmediate 。接下来清空 nextTick 队列。将 setImmediate 添加到队列中。

    console.log(11)

    console.log(12)

    此时的 check 队列是这样的:

    setImmediate(3) setImmediate(7) setImmediate(13)

    接下来按顺序清空 check 队列。打印

    console.log(3)

    console.log(7)

    console.log(13)

    到此为止,执行整个事件循环。那么整体打印内容如下:

    17.png

    总结

    本文主要讲的内容如下:

    原文地址:https://juejin.cn/post/7002106372200333319

    作者:我不是外星人

    更多编程相关知识,请访问:编程视频!!

    以上就是Nodejs进阶学习:深入了解异步I/O和事件循环的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金--我不是外星人,如有侵犯,请联系admin@php.cn删除
    上一篇:浅析Angular中两种类型的Form表单 下一篇:手把手教你使用Node.js进行TCP网络通信(实践)
    大前端线上培训班

    相关文章推荐

    • nodejs怎么修改文件内容• nodejs怎么安装gulp• 使用nodejs如何设置编码• 浅谈Node.js+COW技术进行进程创建和文件复制• 浅谈如何使用Nodejs创建访问日志记录的中间件

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网