深入浅析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 相关文章推荐
JS 建立对象的方法
Apr 21 Javascript
Javascript Ajax异步读取RSS文档具体实现
Dec 12 Javascript
JavaScript显示表单内元素数量的方法
Apr 02 Javascript
js实现跨域的方法实例详解
Jun 24 Javascript
跟我学习javascript的浮点数精度
Nov 16 Javascript
JQuery插件Marquee.js实现无缝滚动效果
Apr 26 Javascript
Javascript在IE和Firefox浏览器常见兼容性问题总结
Aug 03 Javascript
Vue2.x中的Render函数详解
May 30 Javascript
深入理解JavaScript的async/await
Aug 05 Javascript
微信小程序自定义键盘 内部虚拟支付
Dec 20 Javascript
js实现无刷新监听URL的变化示例代码详解
Jun 03 Javascript
解决Vue的文本编辑器 vue-quill-editor 小图标样式排布错乱问题
Aug 03 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
解析PHP中常见的mongodb查询操作
2013/06/20 PHP
详解php的socket通信
2015/08/11 PHP
PHP中substr函数字符串截取用法分析
2016/01/07 PHP
php文件上传后端处理小技巧
2016/05/22 PHP
Laravel5.7框架安装与使用学习笔记图文详解
2019/04/02 PHP
Javascript下的keyCode键码值表
2007/04/10 Javascript
JS Replace()的高级使用方法介绍
2013/06/29 Javascript
jquery实现select选中行、列合计示例
2014/04/25 Javascript
Spring mvc 接收json对象
2015/12/10 Javascript
基于JavaScript实现快速转换文本语言(繁体中文和简体中文)
2016/03/07 Javascript
关于vue.js弹窗组件的知识点总结
2016/09/11 Javascript
JavaScript实现自动切换图片代码
2016/10/11 Javascript
浅析BootStrap中Modal(模态框)使用心得
2016/12/24 Javascript
vue按需加载组件webpack require.ensure的方法
2017/12/13 Javascript
Angular事件之不同组件间传递数据的方法
2018/11/15 Javascript
一秒学会微信小程序制作table表格
2019/02/14 Javascript
bootstrap中的导航条实例代码详解
2019/05/20 Javascript
解决pycharm双击但是无法打开的情况
2020/10/31 Javascript
[44:39]2014 DOTA2国际邀请赛中国区预选赛 NE VS CNB
2014/05/21 DOTA
学习python (1)
2006/10/31 Python
python回调函数用法实例分析
2015/05/09 Python
python 获取list特定元素下标的实例讲解
2018/04/09 Python
如何关掉pycharm中的python console(图解)
2019/10/31 Python
Python中__repr__和__str__区别详解
2019/11/07 Python
python GUI库图形界面开发之PyQt5信号与槽事件处理机制详细介绍与实例解析
2020/03/08 Python
python3.8动态人脸识别的实现示例
2020/09/21 Python
html5指南-6.如何创建离线web应用程序实现离线访问
2013/01/07 HTML / CSS
澳大利亚工具仓库:Tools Warehouse
2018/10/15 全球购物
台湾良兴购物网:EcLife
2019/12/01 全球购物
大学生求职简历的自我评价
2013/10/14 职场文书
2015暑期工社会实践报告
2015/07/13 职场文书
使用react+redux实现计数器功能及遇到问题
2021/06/02 Javascript
SQLServer之常用函数总结详解
2021/08/30 SQL Server
德劲DE1105机评
2022/04/05 无线电
vue-treeselect的基本用法以及解决点击无法出现拉下菜单
2022/04/30 Vue.js
Win11 Beta 22621.601 和 22622.601今日发布 KB5017384修复内容汇总
2022/09/23 数码科技