深入浅析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 相关文章推荐
JavaScript 仿关机效果的图片层
Dec 26 Javascript
10个新的最有前途的JavaScript框架
Mar 12 Javascript
Span元素的width属性无效果原因及解决方案
Jan 15 Javascript
JavaScript生成随机数的4种自定义函数分享
Feb 28 Javascript
jquery实现LED广告牌旋转系统图片切换效果代码分享
Aug 26 Javascript
JavaScript 七大技巧(二)
Dec 13 Javascript
jQuery无刷新上传之uploadify简单代码
Jan 17 Javascript
微信小程序 设置启动页面的两种方法
Mar 09 Javascript
微信小程序 request接口的封装实例代码
Apr 26 Javascript
ionic选择多张图片上传的示例代码
Oct 10 Javascript
详解Vue单元测试Karma+Mocha学习笔记
Jan 31 Javascript
Vue.js实现的表格增加删除demo示例
May 22 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 fsockopen中多线程问题的解决办法[翻译]
2011/11/09 PHP
关于扩展 Laravel 默认 Session 中间件导致的 Session 写入失效问题分析
2016/01/08 PHP
PHP基于mssql扩展远程连接MSSQL的简单实现方法
2016/10/08 PHP
php微信公众平台开发(三)订阅事件处理
2016/12/06 PHP
laravel 框架结合关联查询 when()用法分析
2019/11/22 PHP
初识JQuery 实例一(first)
2011/03/16 Javascript
基于jquery ajax 用户无刷新登录方法详解
2012/04/28 Javascript
JQuery异步获取返回值中文乱码的解决方法
2015/01/29 Javascript
简单的jQuery banner图片轮播实例代码
2016/03/04 Javascript
Web前端新人笔记之jquery入门心得(新手必看)
2016/05/17 Javascript
ionic由于使用了header和subheader导致被遮挡的问题的两种解决方法
2016/09/22 Javascript
vue.js将unix时间戳转换为自定义时间格式
2017/01/03 Javascript
JavaScript运动框架 多值运动(四)
2017/05/18 Javascript
JS随机排序数组实现方法分析
2017/10/11 Javascript
原生JS实现列表内容自动向上滚动效果
2019/05/22 Javascript
JavaScript this关键字指向常用情况解析
2020/09/02 Javascript
Django模型序列化返回自然主键值示例代码
2019/06/12 Python
pytorch 输出中间层特征的实例
2019/08/17 Python
使用TensorFlow-Slim进行图像分类的实现
2019/12/31 Python
Python sorted排序方法如何实现
2020/03/31 Python
django 模版关闭转义方式
2020/05/14 Python
python打开文件的方式有哪些
2020/06/29 Python
Python3基于print打印带颜色字符串
2020/07/06 Python
CSS3中媒体查询结合rem布局适配手机屏幕
2019/06/10 HTML / CSS
乌克兰香水和化妆品网站:Notino.ua
2018/03/26 全球购物
欧尚俄罗斯网上超市:Auchan俄罗斯
2018/05/03 全球购物
分别介绍一下Session Bean和Entity Bean
2015/03/13 面试题
你在项目中用到了xml技术的哪些方面?如何实现的?
2014/01/26 面试题
程序员经常用到的UNIX命令
2015/04/13 面试题
大学生两会学习心得体会
2014/03/10 职场文书
诚信承诺书范文
2014/03/27 职场文书
大学生党员批评与自我批评范文
2014/10/14 职场文书
2014年学校法制宣传日活动总结
2014/11/01 职场文书
2014年学生会部门工作总结
2014/11/07 职场文书
2016秋季小学开学寄语
2015/12/03 职场文书
《走遍天下书为侣》教学反思
2016/02/22 职场文书