深入浅析Node.js 事件循环、定时器和process.nextTick()


Posted in Javascript onOctober 22, 2018

什么是事件循环

尽管JavaScript是单线程的,但通过尽可能将操作放到系统内核执行,事件循环允许Node.js执行非阻塞I/O操作。

由于现代大多数内核都是多线程的,因此它们可以处理在后台执行的多个操作。 当其中一个操作完成时,内核会告诉Node.js,以便可以将相应的回调添加到 轮询队列 中以最终执行。 我们将在本主题后面进一步详细解释。

事件循环解释

当Node.js启动时,它初始化事件循环,处理提供的输入脚本(或放入 REPL ,本文档未涉及),这可能会进行异步API调用,调度计时器或调用 process.nextTick() , 然后开始处理事件循环。

下图显示了事件循环操作顺序的简要概述。

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

注意:每个框都将被称为事件循环的“阶段”。

每个阶段都要执行一个FIFO的回调队列。 虽然每个阶段都有其特殊的方式,但通常,当事件循环进入给定阶段时,它将执行特定于该阶段的任何操作,然后在该阶段的队列中执行回调,直到队列耗尽或最大回调数量为止 。 当队列耗尽或达到回调限制时,事件循环将移至下一阶段,依此类推。

由于这些操作中的任何一个可以调度更多操作并且在轮询阶段中处理的新事件由内核排队,因此轮询事件可以在处理轮询事件时排队。 因此,长时间运行的回调可以允许轮询阶段运行的时间比计时器的阈值长得多。 有关详细信息,请参阅和部分。

注意:Windows和Unix / Linux实现之间存在轻微差异,但这对于此演示并不重要。 最重要的部分在这里。 实际上有七到八个步骤,但我们关心的是 - Node.js实际使用的那些 - 是上面那些。

阶段概述

  • timer : 此阶段执行 setTimeout() 和 setInterval() 调度的回调
  • pending callbacks : 执行延迟到下一个循环迭代的I/O回调
  • idle, prepare : 只用于内部
  • poll : 检索新的I/O事件; 执行与I/O相关的回调(几乎所有回调都是带有异常的 close callbacks , timers 和 setImmediate() 调度的回调); node将在适当的时候阻塞在这里
  • check : 这里调用 setImmediate() 回调函数
  • close callbacks : 一些 close callbacks, 例如. socket.on(‘close', …)

在事件循环的每次运行之间,Node.js检查它是否在等待任何异步I / O或定时器,如果没有,则关闭。

阶段细节

定时器(timer)

计时器在一个回调执行完之后指定阈值,而不是人们希望的确切时间去执行。 定时器回调将在指定的时间过去后尽早安排; 但是,操作系统调度或其他回调的运行可能会延迟它们。

注意:从技术上讲,控制何时执行定时器。

例如,假设您计划在100毫秒后执行 timeout ,然后您的脚本将异步读取一个耗时95毫秒的文件:

const fs = require('fs');
function someAsyncOperation(callback) {
 // Assume this takes 95ms to complete
 fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
 const delay = Date.now() - timeoutScheduled;
 console.log(`${delay}ms have passed since I was scheduled`);
}, 100);
// do someAsyncOperation which takes 95 ms to complete
someAsyncOperation(() => {
 const startCallback = Date.now();
 // do something that will take 10ms...
 while (Date.now() - startCallback < 10) {
 // do nothing
 }
});

当事件循环进入轮询阶段时,它有一个空队列( fs.readFile() 尚未完成),因此它将等待剩余的ms数,直到达到最快的计时器阈值。 当它等待95毫秒传递时, fs.readFile() 完成读取文件,并且其完成需要10毫秒的回调被添加到轮询队列并执行。 当回调结束时,队列中不再有回调,因此事件循环将看到已达到最快定时器的阈值,然后回绕到定时器阶段以执行定时器的回调。 在此示例中,您将看到正在调度的计时器与正在执行的回调之间的总延迟将为105毫秒。

注意:为了防止轮询阶段使事件循环挨饿,libuv(实现Node.js事件循环的C库和平台的所有异步行为)在停止轮询之前也为事件提供了固定的最大值(取决于系统)。

等待回调(pending callbacks)

此阶段执行某些系统操作(例如TCP错误类型)的回调。 例如,如果TCP套接字在尝试连接时收到 ECONNREFUSED ,则某些*nix系统希望等待报告错误。 这将排队等待在等待回调阶段执行。

轮询(poll)

轮询阶段有两个主要功能:

1.计算它阻塞和轮询I / O的时间,然后
2.处理轮询队列中的事件。

当事件循环进入轮询阶段并且没有定时器调度时,将发生以下两种情况之一:

  • 如果轮询队列不为空,则事件循环将遍历回调队列并且同步执行,直到队列已执行完或者达到系统相关的固定限制。
  • 如果轮询队列为空,则会发生以下两种情况之一:

setImmediate()
setImmediate()

检查(check)

此阶段允许在轮询阶段完成后立即执行回调。 如果轮询阶段变为空闲并且脚本已使用 setImmediate() 排队,则事件循环可以继续到检查阶段而不是等待。

setImmediate() 实际上是一个特殊的计时器,它在事件循环的一个单独阶段运行。 它使用libuv API来调度在轮询阶段完成后执行的回调。

通常,在执行代码时,事件循环最终会到达轮询阶段,它将等待传入连接,请求等。但是,如果已使用 setImmediate() 调度回调并且轮询阶段变为空闲,则 将结束并继续检查阶段,而不是等待轮询事件。

关闭回调(close callbacks)

如果套接字或句柄突然关闭( 例如socket.destroy() ),则在此阶段将发出 'close' 事件。 否则它将通过 process.nextTick() 发出。

setImmediate() vs setTimeout()

setImmediate 和 setTimeout() 类似,但根据它们的调用时间以不同的方式运行。

setImmediate()
setTimeout()

执行定时器的顺序将根据调用它们的上下文而有所不同。 如果从主模块中调用两者,则时间将受到进程性能的限制(可能受到计算机上运行的其他应用程序的影响)。

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

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

但是,如果在I / O周期内移动两个调用,则始终首先执行立即回调:

// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
 setTimeout(() => {
 console.log('timeout');
 }, 0);
 setImmediate(() => {
 console.log('immediate');
 });
});
$ node timeout_vs_immediate.js
immediate
timeout
$ node timeout_vs_immediate.js
immediate
timeout

使用 setImmediate() 而不是 setTimeout() 的主要优点是 setImmediate() 将始终在任何定时器之前执行(如果在I / O周期内调度),与存在多少定时器无关。

process.nextTick()

理解 process.nextTick()

您可能已经注意到 process.nextTick() 没有显示在图中,即使它是异步API的一部分。 这是因为 process.nextTick() 在技术上不是事件循环的一部分。 相反, nextTickQueue 将在当前操作完成后处理,而不管事件循环的当前阶段如何。

回顾一下我们的图表,无论何时在给定阶段调用 process.nextTick() ,传递给 process.nextTick() 的所有回调都将在事件循环继续之前得到解决。 这可能会产生一些不好的情况, 因为它允许您通过进行递归的 process.nextTick() 调用来“饿死”您的I / O, 这会阻止事件循环到达轮询阶段。

为什么会被允许?

为什么这样的东西会被包含在Node.js中? 其中一部分是一种设计理念,其中API应该始终是异步的,即使它不是必须的。 以此代码段为例:

function apiCall(arg, callback) {
 if (typeof arg !== 'string')
 return process.nextTick(callback,
    new TypeError('argument should be string'));
}

这段代码进行参数检查,如果不正确,它会将错误传递给回调。 最近更新的API允许将参数传递给 process.nextTick() ,允许它将回调后传递的任何参数作为参数传播到回调,因此您不必嵌套函数。

我们正在做的是将错误传回给用户,但只有在我们允许其余的用户代码执行之后。 通过使用 process.nextTick() ,我们保证 apiCall() 始终在用户代码的其余部分之后和允许事件循环继续之前运行其回调。 为了实现这一点,允许JS调用堆栈展开然后立即执行提供的回调,这允许一个人对 process.nextTick() 进行递归调用而不会达到 RangeError :超出v8的最大调用堆栈大小。

这种理念可能会导致一些潜在的问题。 以此片段为例:

let bar;
// this has an asynchronous signature, but calls callback synchronously
function someAsyncApiCall(callback) { callback(); }
// the callback is called before `someAsyncApiCall` completes.
someAsyncApiCall(() => {
 // since someAsyncApiCall has completed, bar hasn't been assigned any value
 console.log('bar', bar); // undefined
});
bar = 1;

用户将 someAsyncApiCall() 定义为具有异步签名,但它实际上是同步操作的。 调用它时,在事件循环的同一阶段调用提供给 someAsyncApiCall() 的回调,因为 someAsyncApiCall() 实际上不会异步执行任何操作。 因此,回调尝试引用bar,即使它在范围内可能没有该变量,因为该脚本无法运行完成。

通过将回调放在 process.nextTick() 中,脚本仍然能够运行完成,允许在调用回调之前初始化所有变量,函数等。 它还具有不允许事件循环继续的优点。 在允许事件循环继续之前,向用户警告错误可能是有用的。 以下是使用 process.nextTick() 的前一个示例:

let bar;
function someAsyncApiCall(callback) {
 process.nextTick(callback);
}
someAsyncApiCall(() => {
 console.log('bar', bar); // 1
});
bar = 1;

这是另一个真实世界的例子:

const server = net.createServer(() => {}).listen(8080);
server.on('listening', () => {});

仅传递端口时,端口立即绑定。 因此,可以立即调用 'listening' 回调。 问题是那时候不会设置 .on('listening') 回调。

为了解决这个问题, 'listening' 事件在 nextTick() 中排队,以允许脚本运行完成。 这允许用户设置他们想要的任何事件处理程序。

process.nextTick() vs setImmediate()

就用户而言,我们有两个类似的调用,但它们的名称令人困惑。

process.nextTick()
setImmediate()

实质上,应该交换名称。 process.nextTick() 比 setImmediate() 更快地触发,但这是过去创造的,不太可能改变。 进行此切换会破坏npm上的大部分包。 每天都会添加更多新模块,这意味着我们每天都在等待,更多的潜在破损发生。 虽然它们令人困惑,但自身的叫法不会改变。

我们建议开发人员在所有情况下都使用 setImmediate() ,因为它更容易推理(并且它导致代码与更广泛的环境兼容,如浏览器JS。)

为什么要使用 process.nextTick() ?

有两个主要原因:

  • 允许用户处理错误,清除任何不需要的资源,或者在事件循环继续之前再次尝试请求。
  • 有时需要允许回调在调用堆栈展开之后但在事件循环继续之前运行。

一个例子是匹配用户的期望。 简单的例子:

const server = net.createServer();
server.on('connection', (conn) => { });
server.listen(8080);
server.on('listening', () => { });

假设 listen() 在事件循环开始时运行,但是监听回调放在 setImmediate() 中。 除非传递主机名,否则将立即绑定到端口。 要使事件循环继续,它必须达到轮询阶段,这意味着可能已经接收到连接的非零概率允许在侦听事件之前触发连接事件。

另一个例子是运行一个函数构造函数,比如继承自 EventEmitter ,它想在构造函数中调用一个事件:

const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
 EventEmitter.call(this);
 this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
 console.log('an event occurred!');
});

您无法立即从构造函数中发出事件,因为脚本将不会处理到用户为该事件分配回调的位置。 因此,在构造函数本身中,您可以使用 process.nextTick() 来设置回调以在构造函数完成后发出事件,从而提供预期的结果:

const EventEmitter = require('events');
const util = require('util');
function MyEmitter() {
 EventEmitter.call(this);
 // use nextTick to emit the event once a handler is assigned
 process.nextTick(() => {
 this.emit('event');
 });
}
util.inherits(MyEmitter, EventEmitter);
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
 console.log('an event occurred!');
});

总结

以上所述是小编给大家介绍的深入浅析Node.js 事件循环、定时器和process.nextTick() ,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
JScript的条件编译
May 29 Javascript
JQuery 前台切换网站的样式实现
Jun 22 Javascript
AngularJS入门教程之静态模板详解
Aug 18 Javascript
基于vue.js实现图片轮播效果
Dec 01 Javascript
微信小程序 swiper制作tab切换实现附源码
Jan 21 Javascript
Vue组件模板形式实现对象数组数据循环为树形结构(实例代码)
Jul 31 Javascript
vue2 全局变量的设置方法
Mar 09 Javascript
vue配置font-awesome5的方法步骤
Jan 27 Javascript
node微信开发之获取access_token+自定义菜单
Mar 17 Javascript
Vue基本使用之对象提供的属性功能
Apr 30 Javascript
产制造追溯系统之通过微信小程序实现移动端报表平台
Jun 03 Javascript
js实现抽奖功能
Nov 24 Javascript
js实现input密码框显示/隐藏功能
Sep 10 #Javascript
Vue slot用法(小结)
Oct 22 #Javascript
TypeScript基础入门教程之三重斜线指令详解
Oct 22 #Javascript
vue项目中使用Hbuilder打包app 设置沉浸式状态栏的方法
Oct 22 #Javascript
vue-cli项目中使用echarts图表实例
Oct 22 #Javascript
vue使用echarts图表的详细方法
Oct 22 #Javascript
在vue中使用echarts图表实例代码详解
Oct 22 #Javascript
You might like
第八节--访问方式
2006/11/16 PHP
php下关于Cannot use a scalar value as an array的解决办法
2010/08/08 PHP
php加密算法之实现可逆加密算法和解密分享
2014/01/21 PHP
PHP中array_slice函数用法实例详解
2014/11/25 PHP
Jquery 获得服务器控件值的方法小结
2010/05/11 Javascript
JavaScript异步调用定时方法并停止该方法实现代码
2012/03/16 Javascript
js设置cookie过期及清除浏览器对应名称的cookie
2013/10/24 Javascript
JS替换字符串中字符即替换全部而不是第一个
2014/06/04 Javascript
JS实现超简单的鼠标拖动效果
2015/11/02 Javascript
纯js实现页面返回顶部的动画(超简单)
2017/08/10 Javascript
详解AngularJS 过滤器的使用
2018/06/02 Javascript
vue3.0 CLI - 3.2 路由的初级使用教程
2018/09/20 Javascript
vue.js中使用echarts实现数据动态刷新功能
2019/04/16 Javascript
ES6 Promise对象的含义和基本用法分析
2019/06/14 Javascript
如何使用Jquery动态生成二级选项列表
2020/02/06 jQuery
详解Vue 单文件组件的三种写法
2020/02/19 Javascript
JavaScript如何实现图片处理与合成
2020/05/29 Javascript
vue中选中多个选项并且改变选中的样式的实例代码
2020/09/16 Javascript
解决vue项目运行npm run serve报错的问题
2020/10/26 Javascript
Python 文件操作实现代码
2009/10/07 Python
Python cookbook(数据结构与算法)实现对不原生支持比较操作的对象排序算法示例
2018/03/15 Python
python实现一组典型数据格式转换
2018/12/15 Python
Python子进程subpocess原理及用法解析
2020/07/16 Python
GNC健安喜美国官网:美国第一营养品牌
2016/07/22 全球购物
高山背包:High Sierra
2017/11/23 全球购物
毕业生个人求职自荐信
2014/02/26 职场文书
霸王洗发水广告词
2014/03/14 职场文书
环保倡议书500字
2014/05/15 职场文书
霸气押韵的班级口号
2014/06/09 职场文书
四风对照检查材料范文
2014/09/27 职场文书
单位领导婚礼致辞
2015/07/28 职场文书
退休欢送会致辞
2015/07/31 职场文书
OpenCV3.3+Python3.6实现图片高斯模糊
2021/05/18 Python
Logback 使用TurboFilter实现日志级别等内容的动态修改操作
2021/08/30 Java/Android
python使用torch随机初始化参数
2022/03/22 Python
mybatis-plus模糊查询指定字段
2022/04/28 Java/Android