美文网首页
Node.js系列六 - 深入事件循环

Node.js系列六 - 深入事件循环

作者: 懂会悟 | 来源:发表于2021-02-06 08:29 被阅读0次

    1、 Node的事件循环

    1.1. Node的事件循环

    浏览器中的EventLoop是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的。

    libuv到底是什么呢?

    • libuv是一个多平台的专注于异步IO的库,它最初是为Node开发的,但是现在也被使用到Luvit、Julia、pyuv等其他地方;

    1.2. 阻塞IO和非阻塞IO

    如果我们希望在程序中对一个文件进行操作,那么我们就需要打开这个文件:通过文件描述符。

    • 我们思考:JavaScript可以直接对一个文件进行操作吗?
    • 看起来是可以的,但是事实上我们任何程序中的文件操作都是需要进行系统调用(操作系统封装了文件系统);
    • 事实上对文件的操作,是一个操作系统的IO操作(输入、输出);

    操作系统为我们提供了 阻塞式 调用和 非阻塞式 调用:

    • 阻塞式调用: 调用结果返回之前,当前线程处于阻塞态(阻塞态CPU是不会分配时间片的),调用线程只有在得到调用结果之后才会继续执行。
    • 非阻塞式调用: 调用执行之后,当前线程不会停止执行,只需要过一段时间来检查一下有没有结果返回即可。

    所以我们开发中的很多耗时操作,都可以基于这样的 非阻塞式 调用:

    但是非阻塞IO也会存在一定的问题:我们并没有获取到需要读取(我们以读取为例)的结果

    • 那么就意味着为了可以知道是否读取到了完整的数据,我们需要频繁的去确定读取到的数据是否是完整的;
    • 这个过程我们称之为轮询操作

    那么这个轮训的工作由谁来完成呢?

    • 如果我们的主线程频繁的去进行轮训的工作,那么必然会大大降低性能;
    • 并且开发中我们可能不只是一个文件的读写,可能是多个文件;
    • 而且可能是多个功能:网络的IO、数据库的IO、子进程调用;

    libuv提供了一个线程池(Thread Pool):

    • 线程池会负责所有相关的操作,并且会通过轮询等方式等待结果;
    • 当获取到结果时,就可以将对应的回调放到事件循环(某一个事件队列)中;
    • 事件循环就可以负责接管后续的回调工作,告知JavaScript应用程序执行对应的回调函数;

    阻塞和非阻塞,同步和异步有什么区别?

    • 阻塞和非阻塞是对于被调用者来说的;
      • 在我们这里就是系统调用,操作系统为我们提供了阻塞调用和非阻塞调用;
    • 同步和异步是对于调用者来说的;
      • 在我们这里就是自己的程序;
      • 如果我们在发起调用之后,不会进行其他任何的操作,只是等待结果,这个过程就称之为同步调用;
      • 如果我们再发起调用之后,并不会等待结果,继续完成其他的工作,等到有回调时再去执行,这个过程就是异步调用;

    1.3. Node事件循环的阶段

    我们最前面就强调过,事件循环像是一个桥梁,是连接着应用程序的JavaScript和系统调用之间的通道:

    • 无论是我们的文件IO、数据库、网络IO、定时器、子进程,在完成对应的操作后,都会将对应的结果和回调函数放到事件循环(任务队列)中;
    • 事件循环会不断的从任务队列中取出对应的事件(回调函数)来执行;

    但是一次完整的事件循环Tick分成很多个阶段:

    • 定时器(Timers):本阶段执行已经被 setTimeout() 和 setInterval() 的调度回调函数。
    • 待定回调:执行延迟到下一个循环迭代的 I/O 回调。
    • idle, prepare:仅系统内部使用。
    • 轮询(Poll):检索新的 I/O 事件;执行与 I/O 相关的回调;
    • 检测:setImmediate() 回调函数在这里执行。
    • 关闭的回调函数:一些关闭的回调函数,如:socket.on('close', ...)。


      一次tick的事件循环阶段

    我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:

    • 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
    • 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;

    但是,Node中的事件循环不只是 微任务队列和 宏任务队列:

    • 微任务队列:

      • next tick queue:process.nextTick;
      • other queue:Promise的then回调、queueMicrotask;
    • 宏任务队列:

      • timer queue:setTimeout、setInterval;
      • poll queue:IO事件;
      • check queue:setImmediate;
      • close queue:close事件;

    所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:

    • next tick microtask queue;
    • other microtask queue;
    • timer queue;
    • poll queue;
    • check queue;
    • close queue;

    1.4. 实操

    async function async1() {
      console.log('async1 start')
      await async2()
      console.log('async1 end')
    }
    
    async function async2() {
      console.log('async2')
    }
    
    console.log('script start')
    
    setTimeout(function () {
      console.log('setTimeout0')
    }, 0)
    
    setTimeout(function () {
      console.log('setTimeout2')
    }, 300)
    
    setImmediate(() => console.log('setImmediate'));
    
    process.nextTick(() => console.log('nextTick1'));
    
    async1();
    
    process.nextTick(() => console.log('nextTick2'));
    
    new Promise(function (resolve) {
      console.log('promise1')
      resolve();
      console.log('promise2')
    }).then(function () {
      console.log('promise3')
    })
    
    console.log('script end')
    //  result
    script start
    async1 start
    async2
    promise1
    promise2
    script end
    nextTick
    async1 end
    promise3
    
    setTimeout0
    setImmediate
    setTimeout2
    

    相关文章

      网友评论

          本文标题:Node.js系列六 - 深入事件循环

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