目录

  • 微任务
  • 事件循环机制
  • 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执行的整个过程。如果执行了任何非阻塞异步代码(创建计时器、读写文件等),则会进入事件循环。其中事件循环分为六个阶段:

由于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" 在前,也可能 "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')

结果(Node v10.18.0):

分析:

如图 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",符合预期(所有回调执行完毕后,再执行相应微任务)。

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

5.后记

本篇博客所分析的Node.js 的事件循环机制,对于微任务的执行顺序,主要适用于Node 11之前。Node 11及其之后,针对事件循环的每一个阶段,微任务的执行顺序进行了统一,在每次调用回调之后,就执行相应微任务,不会等到所有回调执行完毕后才执行。

6.参考

学习 Node.js,第 5 单元:事件循环

事件循环、计时器和 process.nextTick()

第 25 题:浏览器和Node 事件循环的区别

Node.js 的事件循环机制的更多相关文章

  1. JS JavaScript事件循环机制

    区分进程和线程 进程是cpu资源分配的最小单位(系统会给它分配内存) 不同的进程之间是可以同学的,如管道.FIFO(命名管道).消息队列 一个进程里有单个或多个线程 浏览器是多进程的,因为系统给它的进 ...

  2. 浏览器中 JS 的事件循环机制

    目录 事件循环机制 宏任务与微任务 实例分析 参考 1.事件循环机制 浏览器执行JS代码大致可以分为三个步骤,而这三个步骤的往复构成了JS的事件循环机制(如图). 第一步:主线程(JS引擎线程)中执行 ...

  3. js的事件循环机制:同步与异步任务(setTimeout,setInterval)宏任务,微任务(Promise,process.nextTick)

    javascript是单线程,一切javascript版的"多线程"都是用单线程模拟出来的,通过事件循环(event loop)实现的异步. javascript事件循环 事件循环 ...

  4. Node.js:事件循环

    ylbtech-Node.js:事件循环 1.返回顶部 1. Node.js 事件循环 Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高. Node.js 的每一个 ...

  5. JS浏览器事件循环机制

    文章来自我的 github 博客,包括技术输出和学习笔记,欢迎star. 先来明白些概念性内容. 进程.线程 进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的. ...

  6. JS:事件循环机制、调用栈以及任务队列

    点击查看原文 写在前面 js里的事件循环机制十分有趣.从很多面试题也可以看出来,考察简单的setTimeout也就是考察这个机制的. 在之前,我只是简单地认为由于函数执行很快,setTimeout执行 ...

  7. js的事件循环机制和任务队列

    上篇讲异步的时候,提到了同步队列和异步队列的说法,其实只是一种形象的称呼,分别代表主线程中的任务和任务队列中的任务,那么此篇我们就来详细探讨这两者. 一.来张图感受一下 如果看完觉得一脸懵逼,请继续往 ...

  8. JS基础-事件循环机制

    从一道题浅说 JavaScript 的事件循环 原文链接: https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7 ...

  9. 理解 node.js 的事件循环

    node.js 的第一个基本观点是,I/O 操作是昂贵的: 目前的编程技术最大的浪费来自等待 I/O 操作的完成.有几种方法可以解决这些对性能的影响(来自Sam Rushing): 同步:依次处理单个 ...

随机推荐

  1. 创建Sphinx + GitHub + ReadtheDocs托管文档

    最新博客链接 "Tsanfer's Blog" 创建Sphinx + GitHub + ReadtheDocs托管文档 Readthedocs在线电子书链接

  2. (翻译) 使用Unity进行AOP对象拦截

    Unity 是一款知名的依赖注入容器( dependency injection container) ,其支持通过自定义扩展来扩充功能. 在Unity软件包内 默认包含了一个对象拦截(Interce ...

  3. 目标检测 | RetinaNet:Focal Loss for Dense Object Detection

    论文分析了one-stage网络训练存在的类别不平衡问题,提出能根据loss大小自动调节权重的focal loss,使得模型的训练更专注于困难样本.同时,基于FPN设计了RetinaNet,在精度和速 ...

  4. 被问到到http的时候你就这么回答!

    大家好,我是标题党,啊不,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 什么是互 ...

  5. mycat主要参数

    以下内容源于mycat官方文档,记录下来方便直接查看: mycat版本:1.6 负载均衡类型,目前的取值有 3 种:1. balance="0", 不开启读写分离机制,所有读操作都 ...

  6. Spring-Cloud-Alibaba Nacos 启动失败,窗口一闪而过

    下载及启动 Nacos 下载地址:https://github.com/alibaba/nacos/releases 在Windows下,进入bin目录,双击 startup.cmd 即可运行 启动出 ...

  7. ajax实现图片上传与进度条

    这里使用的是bootstract的一个插件来实现 详情请查看文档中的进度条 https://v3.bootcss.com/components/ 引入必要的文件 <link href=" ...

  8. java面试基础篇-List

    一.ArrayList: 底层为数组实现,线程不安全,查询,修改快,增加删除慢, 数据结构:数组以0为下标依次连续进行存储 数组查询元素:根据下标查询就行 数组增加元素:如果需要给index为10的位 ...

  9. tkinter gui控件回调和grid布局优化

    0.引子 Tkinter 是 Python 的标准 GUI 库.Python 使用 Tkinter 可以快速的创建 GUI 应用程序.由于 Tkinter 是内置到 python 的安装包中.只要安装 ...

  10. 小案例带你揭秘JS事件

    小案例带你揭秘JS事件 ### 什么是事件? 在js中一个事件的组成由那些呢? 谁触发事件:事件源 触发什么事件: 事件的类型 触发事件干什么事:事件处理函数 事件传播的过程 捕获阶段 就是从wind ...