深入浅析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 监听textarea中按键事件
Oct 08 Javascript
jquery 仿QQ校友的DIV模拟窗口效果源码
Mar 24 Javascript
WEB 浏览器兼容 推荐收藏
May 14 Javascript
js定时调用方法成功后并停止调用示例
Apr 08 Javascript
node.js中的querystring.escape方法使用说明
Dec 10 Javascript
jQuery三级下拉列表导航菜单代码分享
Apr 15 Javascript
果断收藏9个Javascript代码高亮脚本
Jan 06 Javascript
javascript中加var和不加var的区别 你真的懂吗
Jan 06 Javascript
js文件中直接alert()中文出来的是乱码的解决方法
Nov 01 Javascript
9102年webpack4搭建vue项目的方法步骤
Feb 20 Javascript
详解如何更好的使用module vuex
Mar 27 Javascript
详解vue中v-model和v-bind绑定数据的异同
Aug 10 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
回首过去10年中最搞笑的10部动漫,哪一部让你节操尽碎?
2020/03/03 日漫
谈谈PHP语法(2)
2006/10/09 PHP
php上传、管理照片示例
2006/10/09 PHP
破解图片防盗链的代码(asp/php)测试通过
2010/07/02 PHP
一个典型的PHP分页实例代码分享
2011/07/28 PHP
如何解决phpmyadmin导入数据库文件最大限制2048KB
2015/10/09 PHP
Yii2.0高级框架数据库增删改查的一些操作
2015/11/16 PHP
Laravel框架基于ajax和layer.js实现无刷新删除功能示例
2019/01/17 PHP
不错的新闻标题颜色效果
2006/12/10 Javascript
JQuery获取当前屏幕的高度宽度的实现代码
2011/07/12 Javascript
早该知道的7个JavaScript技巧
2013/03/27 Javascript
js切换光标示例代码
2013/10/10 Javascript
jQuery替换字符串(实例代码)
2013/11/13 Javascript
javascript面向对象快速入门实例
2015/01/13 Javascript
jQuery 选择器详解
2015/01/19 Javascript
实例讲解jQuery EasyUI tree中state属性慎用
2016/04/01 Javascript
功能强大的Bootstrap使用手册(一)
2016/08/02 Javascript
基于bootstrap风格的弹框插件
2016/12/28 Javascript
微信小程序多张图片上传功能
2017/06/07 Javascript
十个免费的web前端开发工具详细整理
2017/09/18 Javascript
Angular2 父子组件通信方式的示例
2018/01/29 Javascript
详解如何解决Vue和vue-template-compiler版本之间的问题
2018/09/17 Javascript
微信小程序实现通过双向滑动缩放图片大小的方法
2018/12/30 Javascript
Python中matplotlib中文乱码解决办法
2017/05/12 Python
Python使用smtp和pop简单收发邮件完整实例
2018/01/09 Python
TensorFlow 滑动平均的示例代码
2018/06/19 Python
Numpy截取指定范围内的数据方法
2018/11/14 Python
Python提取特定时间段内数据的方法实例
2019/04/01 Python
详解Numpy数组转置的三种方法T、transpose、swapaxes
2019/05/27 Python
CSS3 clip-path 用法介绍详解
2018/03/01 HTML / CSS
暑期实习鉴定
2013/12/16 职场文书
通用自荐信范文
2014/03/14 职场文书
关于保护环境的建议书
2014/05/13 职场文书
建筑工地标语
2014/06/18 职场文书
2014年医药代表工作总结
2014/11/22 职场文书
幼儿园教师暑期培训心得体会
2016/01/09 职场文书