`
niuzen15
  • 浏览: 3501 次
文章分类
社区版块
存档分类
最新评论

Node.js 的事件循环机制

 
阅读更多

目录

  • 微任务
  • 事件循环机制
  • setImmediate、setTimeout/setInterval 和 process.nextTick 执行时机对比
  • 实例分析
  • 参考

 

1.微任务

在谈论Node的事件循环机制之前,先补充说明一下 Node 中的“微任务”。这里说的微任务(microtasks)其实是一个统称,包含了两部分:

  • process.nextTick() 注册的回调 (nextTick task queue)
  • promise.then() 注册的回调 (promise task queue)

Node 在执行微任务时, 会优先执行 nextTick task queue 中的任务,执行完之后会接着执行 promise task queue 中的任务。所以如果 process.nextTick 的回调与 promise.then 的回调都处于主线程或事件循环中的同一阶段, process.nextTick 的回调要优先于 promise.then 的回调执行。

 

2.事件循环机制

Node事件循环

如图,表示Node执行的整个过程。如果执行了任何非阻塞异步代码(创建计时器、读写文件等),则会进入事件循环。其中事件循环分为六个阶段:

由于Pending callbacks、Idle/Prepare 和 Close callbacks 阶段是 Node 内部使用的三个阶段,所以这里主要分析与开发者代码执行更为直接关联的Timers、Poll 和 Check 三个阶段。

 

Timers(计时器阶段):从图可见,初次进入事件循环,会从计时器阶段开始。此阶段会判断是否存在过期的计时器回调(包含 setTimeout 和 setInterval),如果存在则会执行所有过期的计时器回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Pending callbacks 阶段。

Pending callbacks:执行推迟到下一个循环迭代的I / O回调(系统调用相关的回调)。

Idle/Prepare:仅供内部使用。(详略)

Poll(轮询阶段):

当回调队列不为空时:

会执行回调,若回调中触发了相应的微任务,这里的微任务执行时机和其他地方有所不同,不会等到所有回调执行完毕后才执行,而是针对每一个回调执行完毕后,就执行相应微任务。执行完所有的回到后,变为下面的情况。

当回调队列为空时(没有回调或所有回调执行完毕):

但如果存在有计时器(setTimeout、setInterval和setImmediate)没有执行,会结束轮询阶段,进入 Check 阶段。否则会阻塞并等待任何正在执行的I/O操作完成,并马上执行相应的回调,直到所有回调执行完毕。

Check(查询阶段):会检查是否存在 setImmediate 相关的回调,如果存在则执行所有回调,执行完毕后,如果回调中触发了相应的微任务,会接着执行所有微任务,执行完微任务后再进入 Close callbacks 阶段。

Close callbacks:执行一些关闭回调,比如 socket.on('close', ...)等。

 

总结&注意:

  1. 每一个阶段都会有一个FIFO回调队列,都会尽可能的执行完当前阶段中所有的回调或到达了系统相关限制,才会进入下一个阶段。
  2. Poll 阶段执行的微任务的时机和 Timers 阶段 & Check 阶段的时机不一样,前者是在每一个回调执行就会执行相应微任务,而后者是会在所有回调执行完之后,才统一执行相应微任务。

 

3.setImmediate、setTimeout/setInterval 和 process.nextTick 执行时机对比

setImmediate:触发一个异步回调,在事件循环的 Check 阶段立即执行。

setTimeout:触发一个异步回调,当计时器过期后,在事件循环的 Timers 阶段执行,只执行一次(可用 clearTimeout 取消)。

setInterval:触发一个异步回调,每次计时器过期后,都会在事件循环的 Timers 阶段执行一次回调(可用 clearInterval 取消)。

process.nextTick:触发一个微任务(异步)回调,既可以在主线程(mainline)中执行,可以存在事件循序的某一个阶段中执行。

 

4.实例分析

第一组:

比较 setTimeout 与 setImmediate:

// test.js
setTimeout(() => {
  console.log('setTimeout');
}, 0);

setImmediate(() => {
  console.log('setImmediate');
});

结果:

setTimeout vs setImmediate

分析:

从输出结果来看,输出是不确定的,既可能 "setTimeout" 在前,也可能 "setImmediate" 在前。从事件循环的流程来分析,事件循环开始,会先进入 Timers 阶段,虽然 setTimeout 设置的 delay 是 0,但其实是1,因为 Node 中的 setTimeout 的 delay 取值范围必须是在 [1, 2^31-1] 这个范围内,否则默认为1,因此受进程性能的约束,执行到 Timers 阶段时,可能计时器还没有过期,所以继续向下一个流程进行,所以会偶尔出现 "setImmediate" 输出在前的情况。如果适当地调大 setTimeout 的 delay,比如10,则基本上必然是 "setImmediate" 输出在前面。

第二组:

比较主线程(mainline)、Timers 阶段、Poll 阶段和 Check 阶段的回调执行以及对应的微任务执行的顺序:

 // test.js
 const fs = require('fs');

 console.log('mainline: start')
 process.nextTick(() => {
   console.log('mainline: ', 'process.nextTick\n')
 })

let counter = 0;
const interval = setInterval(() => {
  console.log('timers: setInterval.start ', counter)
  if(counter < 2) {
    setTimeout(() => {
      console.log('timers: setInterval.setTimeout')
      process.nextTick(() => {
        console.log('timers microtasks: ', 'setInterval.setTimeout.process.nextTick\n')
      })
    }, 0)

    fs.readdir('./', (err, files) => {
      console.log('poll: setInterval.readdir1')
      process.nextTick(() => {
        console.log('poll microtasks: ', 'setInterval.readdir1.process.nextTick')
        process.nextTick(() => {
          console.log('poll microtasks: ', 'setInterval.readdir1.process.nextTick.process.nextTick')
        })
      })
    })

    fs.readdir('./', (err, files) => {
      console.log('poll: setInterval.readdir2')
      process.nextTick(() => {
        console.log('poll microtasks: ', 'setInterval.readdir2.process.nextTick')
        process.nextTick(() => {
          console.log('poll microtasks: ', 'setInterval.readdir2.process.nextTick.process.nextTick\n')
        })
      })
    })

    setImmediate(() => {
      console.log('check: setInterval.setImmediate1')
      process.nextTick(() => {
        console.log('check microtasks: ', 'setInterval.setImmediate1.process.nextTick')
      })
    })

    setImmediate(() => {
      console.log('check: setInterval.setImmediate2')
      process.nextTick(() => {
        console.log('check microtasks: ', 'setInterval.setImmediate2.process.nextTick\n')
      })
    })
  } else {
    console.log('timers: setInterval.clearInterval')
    clearInterval(interval)
  }

  console.log('timers: setInterval.end ', counter)
  counter++;
}, 0);

 console.log('mainline: end')

结果:

不同阶段的回调执行以及对应的微任务执行的顺序

分析:

如图 mainline:可以看到,主线程中的 process.nextTick 是在同步代码执行完之后以及在事件循环之前执行,符合预期。

如图 第一次 timers:此时事件循环第一次到 Timers 阶段,setInterval 的 delay 时间到了,所以执行回调,由于没有触发直接相应的微任务,所以直接进入后面的阶段。

如图 第一次 poll:此时事件循环第一次到 Poll 阶段,由于之前 Timers 阶段执行的回调中,触发了两个非阻塞的I/O操作(readdir),在这一阶段时I/O操作执行完毕,直接执行了对应的两个回调。从输出可以看出,针对每一个回调执行完毕后,就执行相应微任务,微任务中再次触发微任务也会继续执行,并不会等到所有回调执行完后再去触发微任务,符合预期。执行完毕所有回调之后,因为还有调度了计时器,所以 Poll 阶段结束,进入 Check 阶段。

如图 第一次 check:此时事件循环第一次到 Check 阶段,直接触发对应的两个 setImmediate 执行。从输出可以看出,微任务是在所有的回调执行完毕之后才触发执行的,符合预期。执行完微任务后,进入后面阶段。

如图 第二次 timers:此时事件循环第二次到 Timers 阶段,首先输出了 "timers: setInterval.setTimeout" ,这是为什么?不要忘了,之前第一次执行 setInterval 的回调时,其实已经执行了一次其内部的 setTimeout(..., 0),但由于它并不能触发微任务,所以其回调没有被执行,而是进入到了后面的阶段,而是等到再次来到 Timers 阶段,根据FIFO,优先执行之前的 setTimeout 的回调,再执行 setInterval 的回调,而最后等所有回调执行完毕,再执行 setTimeout 的回调里面触发的微任务,最后输出的是 "timers microtasks: setInterval.setTimeout.process.nextTick",符合预期(所有回调执行完毕后,再执行相应微任务)。

后面的输出类似,所以不再做过多分析。

 

分享到:
评论

相关推荐

    node.js(v16.16.0) 安装包

    此外,Node.js 内置了事件驱动架构,通过事件循环机制处理异步操作,减少了资源消耗。 V8 引擎的更新也是每次 Node.js 版本升级的重点。在 v16.16.0 中,V8 可能已经包含了最新的优化,比如更快的垃圾回收算法,...

    node.js 安装包 10.16.3-x64

    - **单线程**:尽管Node.js在底层多线程处理,但对外表现为单线程模型,降低了复杂性,同时通过事件循环机制处理并发请求。 - **模块化**:Node.js 提供了一套强大的模块化系统,便于代码复用和项目管理。 2. **...

    Node.js硬实战 115个核心技巧.pdf

    2. **事件循环(Event Loop)**:Node.js的运行机制基于事件循环,理解这一机制对于优化代码和解决并发问题至关重要。书中会介绍事件循环的工作原理,以及如何利用process.nextTick、setImmediate和setTimeout等方法...

    node.js实战 pdf+源码

    本书的核心内容围绕JavaScript的非阻塞I/O模型,利用Node.js的异步事件驱动架构,来实现高性能的网络应用。以下是基于标题、描述和标签所涵盖的多个关键知识点的详细解释: 1. **Node.js基础**:Node.js是一个基于...

    Node.js-深入理解Node.js核心思想与源码分析

    2. 事件驱动:libuv的事件循环机制与Node.js的事件模型紧密相连,负责调度和分发事件,实现异步处理。 3. 文件系统操作:libuv封装了异步文件操作API,如读写文件、创建目录等,确保在I/O密集型任务中不阻塞主线程。...

    Node.js-《Node.js调试指南》

    Node.js的事件循环和异步编程是其核心特性之一,因此调试异步代码至关重要。学会使用`Promise`和`async/await`的`catch`块以及`try/catch`结构来捕获并处理异常,同时利用`setTimeout`和`setInterval`的回调函数调试...

    Node.js Design Patterns, 2nd Edition.pdf

    - **异步编程**:介绍Node.js中异步编程的基础概念,包括回调函数、事件循环等。 - **模块系统**:详细解释Node.js中的模块系统,如何创建和使用模块。 - **文件系统操作**:教授如何使用Node.js进行文件读写、文件...

    node.js源码 node-v21.0.0.tar.gz

    通过研究"node-v21.0.0"源码,我们可以了解Node.js的内部结构,学习如何实现事件循环、非阻塞I/O等核心特性,以及如何优化和调试Node.js应用程序。这对于进阶Node.js开发,甚至是贡献到Node.js社区都是非常有价值的...

    Node.js初级+进阶

    Node.js是一种基于Chrome V8引擎的JavaScript运行环境,它让JavaScript可以在服务器端执行,打破了JavaScript只能在浏览器中运行的传统。本教程旨在帮助初学者及有一定基础的学习者深入理解并掌握Node.js的核心概念...

    Node.js-一个能在单独的线程中执行Node.js函数的零依赖库

    这通常意味着它提供了一种轻量级的方式来并行执行任务,避免了Node.js单线程事件循环模型中可能出现的阻塞问题。 Node.js作为一个基于Chrome V8引擎的JavaScript运行环境,它的默认设计是单线程的,主要依赖异步I/O...

    Node.js-Node.js的源代码和解析缓存

    - **src**: 存放 C++ 源代码,这是 Node.js 与 V8 引擎交互的主要接口,包括事件循环、异步操作等实现。 - **deps**: 依赖库,如 V8 引擎、OpenSSL 等。 - **tools**: 构建脚本和其他辅助工具。 通过阅读源代码,...

    Node.js.the.Right.Way(2013.11)

    Node.js生态系统中重要组成部分之一是事件循环机制。事件循环允许Node.js执行非阻塞I/O操作,即代码可以发起I/O操作而不必等待结果返回,然后继续执行后续代码。当I/O操作完成时,会通过回调函数通知Node.js,然后由...

    Node.JS介紹

    - **事件循环(Event Loop)**: Node.JS通过事件循环机制来实现其非阻塞IO模型。当一个请求到达时,Node.JS会将其注册到事件队列中,然后继续处理其他请求。一旦某个请求完成,事件循环会检查队列并将控制权交给相应的...

    Node.js-StuQ分享专题《深入浅出jsNode.js异步流程控制》完整版

    在Node.js中,异步处理主要通过回调函数、事件循环、Promise和async/await等机制实现。 1. **回调函数**:在早期的Node.js开发中,回调是最常见的异步处理方式。当一个操作完成时,会调用预先定义好的函数,以处理...

    packt Node.JS Cookbook 2nd with source code

    1. **基础概念**:介绍Node.js的基础知识,如模块系统、CommonJS规范、Node.js的事件循环机制以及非阻塞I/O的工作原理。 2. **文件系统操作**:讲解如何使用Node.js进行文件读写、目录操作,包括异步和同步的方法,...

Global site tag (gtag.js) - Google Analytics