IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> JS 事件循环 Node 篇 -> 正文阅读

[JavaScript知识库]JS 事件循环 Node 篇

JS 事件循环 Node 篇

之前介绍过浏览器中的事件循环,本文将详细介绍 Node 中的事件循环。

Node 中的事件循环比起浏览器中的 JavaScript 还是有一些区别的,各个浏览器在底层的实现上可能有些细微的出入;而 Node 只有一种实现,相对起来就少了一些理解上的麻烦。

首先要明确的是,事件循环同样运行在单线程环境下,JavaScript 的事件循环是依靠浏览器实现的,而Node 作为另一种运行时,事件循环由底层的 libuv 实现。

根据 Node.js 官方介绍,每次事件循环都包含了6个阶段,如下图所示

image-20220404145555012

注意:每个框被称为事件循环机制的一个阶段。

每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段,等等。

阶段概述

  • timers 阶段:这个阶段执行timer(setTimeoutsetInterval)的回调
  • I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
  • idle, prepare 阶段:仅node内部使用
  • poll 阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段:执行 setImmediate() 的回调
  • close callbacks 阶段:执行一些关闭的回调函数,如:socket.on('close', ...)

阶段的详细概述

timers 阶段

timers 是事件循环的第一个阶段,Node 会去检查有无已过期的 timer,如果有则把它的回调压入 timer的任务队列中等待执行,事实上,Node 并不能保证 timer 在预设时间到了就会立即执行,因为 Node 对timer的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲。比如下面的代码,setTimeout()setImmediate() 的执行顺序是不确定的。

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

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

但是把它们放到一个I/O回调里面,就一定是 setImmediate() 先执行,因为poll阶段后面就是check阶段。

I/O callbacks 阶段

官方文档对这个阶段的描述为除了timers、setImmediate,以及 close 操作之外的大多数的回调方法都位于这个阶段执行。事实上从源码来看,该阶段只是用来执行pending callback,例如一个TCP socket执行出现了错误,在一些*nix系统下可能希望稍后再处理这里的错误,那么这个回调就会放在IO callback阶段来执行。

一些常见的回调,例如 fs.readFile 的回调是放在 poll 阶段来执行的。

poll 阶段

poll 阶段主要有2个功能:

  • 处理 poll 队列的事件
  • 当有已超时的 timer,执行它的回调函数

even loop 将同步执行 poll 队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate(),分两种情况:

  1. 若有预设的setImmediate(), event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列
  2. 若没有预设的setImmediate(),event loop将阻塞在该阶段等待,等待新的事件出现,这也是该阶段为什么会被命名为 poll(轮询) 的原因。

注意一个细节,没有setImmediate()会导致event loop阻塞在poll阶段,这样之前设置的timer岂不是执行不了了?所以咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timers阶段

check 阶段

setImmediate是一个特殊的定时器方法,它占据了事件循环的一个阶段,整个check 阶段就是为setImmediate方法而设置的。

setImmediate()的回调会被加入check队列中, 从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。

close callbacks 阶段

如果一个 socket 或者一个句柄被关闭,那么就会产生一个close事件,该事件会被加入到对应的队列中。clos阶段执行完毕后,本轮事件循环结束,循环进入到下一轮。

小结

看完了上面的描述,我们明白了 Node 中的event loop 是分阶段处理的,对于每一阶段来说,处理事件队列中的事件就是执行对应的回调方法,每一阶段的event loop 都对应着不同的队列。当 event loop 到达某个阶段时,将执行该阶段的任务队列,直到队列清空或执行的回调达到系统上限后,才会转入下一个阶段。当所有阶段被顺序执行一次后,称 event loop 完成了一个 tick

Node.js 与浏览器的 Event Loop 差异

浏览器环境下,microtask的任务队列是每个macrotask执行完之后执行。

浏览器端

而在Node.js中,microtask会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行microtask队列的任务。Node.js端

setImmediate 对比 setTimeout

setImmediate()setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。

  • setImmediate() 设计为一旦在当前 poll 阶段 阶段完成,就执行脚本。
  • setTimeout() 在最小阈值(ms 单位)过后运行脚本。

执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时器将受进程性能的约束(这可能会受到计算机 上其他正在运行应用程序的影响)。

例如,如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束:

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

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

// timeout
// immediate

// or

// immediate
// timeout

但是,如果你把这两个函数放入一个 I/O 循环内调用,setImmediate 总是被优先调用:

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

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});

// immediate
// timeout

使用 setImmediate() 相对于setTimeout() 的主要优势是,如果setImmediate()是在 I/O 周期内被调度的,那它将会在其中任何的定时器之前执行,跟这里存在多少个定时器无关。

process.nextTick

process.nextTick 的意思就是定义出一个异步动作,并且让这个动作在事件循环当前阶段结束后执行。

例如下面的代码,将打印first的操作放在nextTick的回调中执行,最后先打印出next,再打印first

process.nextTick(function() {
  console.log('first');
});
console.log('next');

// next
// first

process.nextTick其实并不是事件循环的一部分,但它的回调方法也是由事件循环调用的,该方法定义的回调方法会被加入到名为nextTickQueue的队列中。在事件循环的任何阶段,如果nextTickQueue不为空,都会在当前阶段操作结束后优先执行nextTickQueue中的回调函数,当nextTickQueue中的回调方法被执行完毕后,事件循环才会继续向下执行。

Node 限制了nextTickQueue的大小,如果递归调用了process..nextTick,那么当nextTickQueue达到最大限制后会抛出一个错误,我们可以写一段代码来证实这一点。

function recurse(i) {
  while(i < 9999) {
    process.nextTick(recurse(i++));
  }
}
recurse(0);

运行上面代码会报错:

RangeError: Maximum call stack size exceeded

既然nextTickQueue也是一个队列,那么先被加入队列的回调会先执行,我们可以定义多个process.nextTick,然后观察他们的执行顺序:

process.nextTick(function () {
  console.log('first');
});
process.nextTick(function() {
  console.log('second');
});
console.log('next');
// next
// first
// second

和其他回调函数一样,nextTick定义的回调也是由事件循环执行的,如果nextTick的回调方法中出现了阻塞操作,后面的要执行的回调同样会被阻塞。

process.nextTick(function () {
  console.log('first');
  // 由于死循环的存在,之后的事件被阻塞
  while(true) { }
});
process.nextTick(function() {
  console.log('second');
});
console.log('next');
// 依次打印 next first,不会打印 second

nextTick VS setlmmediate

setImmediate方法不属于ECMAScript标准,而是Node提出的新方法,它同样将一个回调函数加入到事件队列中,不同于setTimeoutsetIntervalsetlmmediate并不接受一个时间作为参数,setlmmediate的事件会在当前事件循环的结尾触发,对应的回调方法会在当前事件循环末尾check 阶段执行。

setImmediate方法和process.nextTick方法很相似,二者经常被拿来放在一起比较,从语义角度看,setImmediate() 应该比 process.nextTick() 先执行才对,而事实相反,由于process.nextTick会在当前操作完成后立刻执行,因此总会在setImmediate之前执行。

此外,当有递归的异步操作时只能使用setlmmediate,不能使用process.nextTick,前面已经展示过了递归调用nextTick会出现的错误,下面使用setlmmediate来试试看:

function recurse(i, end) {
  if (i < end) {
    console.log('Done');
  } else {
    console.log(i);
    setImmediate(recurse, i + 1, end);
  }
}
recurse(0, 9999999);

完全没问题!这是因为setImmediate不会生成call stack

总结

  1. Node.js 的事件循环分为6个阶段
  2. 浏览器和Node 环境下,microtask任务队列的执行时机不同
    • Node.js中,microtask 在事件循环的各个阶段之间执行
    • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行
  3. 递归的调用process.nextTick()会导致I/O starving,官方推荐使用setImmediate()
  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2022-04-06 16:08:57  更:2022-04-06 16:11:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 2:58:43-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码