搜索
首页web前端js教程Node.js 事件循环

Node.js 事件循环

Nov 18, 2016 pm 12:50 PM
node.js

什么是事件循环(Event Loop)

 

事件循环能让 Node.js 执行非阻塞 I/O 操作 -- 尽管JavaScript事实上是单线程的 -- 通过在可能的情况下把操作交给操作系统内核来实现。

 

由于大多数现代系统内核是多线程的,内核可以处理后台执行的多个操作。当其中一个操作完成的时候,内核告诉 Node.js,相应的回调就被添加到轮询队列(poll queue)并最终得到执行。本主题随后会解释更多相关细节。

 

 

事件循环

 

Node.js 开始的时候会初始化事件循环,处理目标脚本,脚本可能会进行异步API调用、定时任务或者process.nextTick(),然后开始进行事件循环。

 

下面的表格简要描述了事件循环的操作顺序。

wKiom1gtudmz3u2sAAEuTTwV-g4258.png-wh_651x-s_2584281609.png

注:每个方框代表事件循环中的一个阶段。

 

每个阶段都有一个需要执行的回调函数的先入先出(FIFO)队列。同时,每个阶段都是特殊的,基本上,当事件循环进行到某个阶段时,会执行该阶段特有的操作,然后执行该阶段队列中的回调,直到队列空了或者达到了执行次数限制。这时候,事件循环会进入下一个阶段,循环往复。

 

由于这些操作可能产生更多的计划任务操作,并且轮询阶段处理的新事件会被加入到内核的队列,轮询事件被处理的时候会有新的轮询事件加入。于是,长时回调任务会导致轮询阶段的时间超过了定时器的阈值。 详情见 定时器(timers)和轮询(poll)部分。

 

注:Windows 和 Unix/Linux 的实现有轻微的矛盾之处,但并不影响刚才的描述。 最重要的部分都有了。实际上有七八个阶段,但我们关注的 -- Node.js 实际使用的 -- 就是上面这些。

 

 

阶段总览 (Phases Overview)

 

计时器(timers):本阶段执行setTimeout() 和 setInterval() 计划的回调;

I/O 回调: 执行几乎全部发生异常的 close 回调, 由定时器和setImmediate()计划的回调;

空闲,预备(idle,prepare):只内部使用;

轮询(poll): 获取新的 I/O 事件;nodejs这时会适当进行阻塞;

检查(check): 调用 setImmediate() 的回调;

close callbacks: 例如 socket.on('close', ... );

 

在事件循环运行之间,Node.js 检查是否有正在等待的异步I/O 或者定时器,如果没有就清除并结束。

 

 

阶段细节

 

定时器(timers)

 

定时器的用途是让指定的回调函数在某个阈值后会被执行,具体的执行时间并不一定是那个精确的阈值。定时器的回调会在制定的时间过后尽快得到执行,然而,操作系统的计划或者其他回调的执行可能会延迟该回调的执行。

 

注:从技术上来看,轮询阶段控制了定时器的执行时机。

 

例如,你设定了在100ms后执行某个操作,然后脚本开始执行一个需要95ms的文件读取操作:

var fs = require('fs');

function someAsyncOperation (callback) {
  // Assume this takes 95ms to complete
  fs.readFile('/path/to/file', callback);
}

var timeoutScheduled = Date.now();

setTimeout(function () {

  var delay = Date.now() - timeoutScheduled;

  console.log(delay + "ms have passed since I was scheduled");
}, 100);


// do someAsyncOperation which takes 95 ms to completesomeAsyncOperation(function () {

  var startCallback = Date.now();

  // do something that will take 10ms...
  while (Date.now() - startCallback < 10) {
    ; // do nothing
  }

});

当事件循环进入轮询阶段时,队列是空的(fs.readFile()还没完成),因此时间会继续流逝知道最快的定时器需要执行。过了95ms后,fs.readFile() 读完文件了,它的回调被添加到轮询队列,这个回调需要执行10ms。等到这个回调执行完,队列中没有回调了,这时事件循环看到了最近到时的定时器,然后回到定时器阶段(timers phase)来执行之前的定时器回调。

在这个例子中,从定义定时器到回调执行中间过了105ms。

 

注:为了防止轮询阶段持续时间太长,libuv 会根据操作系统的不同设置一个轮询的上限。

 

 

I/O callbacks

 

这个阶段执行一些诸如TCP错误之类的系统操作的回调。例如,如果一个TCP socket 在尝试连接时收到了 ECONNREFUSED错误,某些 *nix 系统会等着报告这个错误。这个就会被排到本阶段的队列中。

 

 

轮询(poll)

 

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

1,执行已经到时的定时器脚本,然后

2,处理轮询队列中的事件。

 

当事件循环进入到轮询阶段却没有发现定时器时:

如果轮询队列非空,事件循环会迭代回调队列并同步执行回调,直到队列空了或者达到了上限(前文说过的根据操作系统的不同而设定的上限)。

如果轮询队列是空的:


如果有setImmediate()定义了回调,那么事件循环会终止轮询阶段并进入检查阶段去执行定时器回调;

如果没有setImmediate(),事件回调会等待回调被加入队列并立即执行。

 

一旦轮询队列空了,事件循环会查找已经到时的定时器。如果找到了,事件循环就回到定时器阶段去执行回调。

 

 

检查(check)

 

这个阶段允许回调函数在轮询阶段完成后立即执行。如果轮询阶段空闲了,并且有回调已经被 setImmediate() 加入队列,事件循环会进入检查阶段而不是在轮询阶段等待。

 

setImmediate() 是个特殊的定时器,在事件循环中一个单独的阶段运行。它使用libuv的API 来使得回调函数在轮询阶段完成后执行。

 

基本上,随着代码的执行,事件循环会最终进入到等待状态的轮询阶段,可能是等待一个连接、请求等。然而,如果有一个setImmediate() 设置了一个回调并且轮询阶段空闲了,那么事件循环会进入到检查阶段而不是等待轮询事件。   ---- 这车轱辘话说来说去的

 

 

关闭事件的回调(close callbacks)

 

如果一个 socket 或句柄(handle)被突然关闭(is closed abruptly),例如 socket.destroy(), 'close' 事件会被发出到这个阶段。否则这种事件会通过 process.nextTick() 被发出。

 

 

setImmediate() vs setTimeout()

 

这两个很相似,但调用时机会的不同会导致它们不同的表现。

 

setImmediate() 被设计成一旦轮询阶段完成就执行回调函数;

setTimeout() 规划了在某个时间值过后执行回调函数;

 

这两个执行的顺序会因为它们被调用时的上下文而有所不同。如果都是在主模块调用,那么它们会受到进程性能的影响(运行在本机的其他程序会影响它们)。

 

例如,如果我们在非 I/O 循环中运行下面的脚本(即在主模块中),他俩的顺序是不固定的,因为会受到进程性能的影响:

// timeout_vs_immediate.jssetTimeout(function timeout () {
  console.log(&#39;timeout&#39;);
},0);

setImmediate(function immediate () {
  console.log(&#39;immediate&#39;);
});

$ node timeout_vs_immediate.js

timeout

immediate


$ node timeout_vs_immediate.js

immediate

timeout

但是如果把它们放进 I/O 循环中,setImmediate() 的回调总是先执行:

// timeout_vs_immediate.jsvar fs = require(&#39;fs&#39;)

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

$ 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。这是因为它技术上不属于事件循环。然而,nextTickQueue 会在当前操作结束后被处理,不管是在事件循环的哪个阶段。

 

回头看看之前那个表格,你在某个阶段的任何时候调用它,它的所有回调函数都会在事件循环继续进行之前得到处理。有时候这会导致比较糟糕的情况,因为它允许你用递归调用的方式去“阻塞” I/O,这会让事件循环无法进入到轮询阶段。

 

为什么要允许这样

 

部分是因为 Node.js 的设计哲学:API 应该总是异步的,即使本不需要是异步。

 

blablabla,后面几段看的我有点尴尬+晕。既尴尬又晕是觉得这几段说的有点啰嗦,而且举的例子不合适。例子要么是同步的,不是异步的。要么是例子里的写法完全可以避免,比如应该先添加 'connect' 事件监听再进行 .connect() 操作;又或者变量声明最好放在变量使用之前,可以避免变量的提前声明和当时赋值的麻烦。

 

难道是我没理解里面的秘辛?

 

 

process.nextTick() vs setTimeout()

 

这两个函数有些相似但是名字让人困惑:

process.netxtTick() 在事件循环的当前阶段立即生效;

setImmediate() 生效是在接下来的迭代或者事件循环的下一次tick;

 

本质上,它们的名字应该互换一下。process.nextTick() 比 setImmediate() 更“立刻”执行,但这是个历史问题没法改变。如果改了,npm上大堆的包就要挂了。

 

我们推荐开发者在所有情况下都使用 setImmediate() 因为它更显而易见(reason about),另外兼容性也更广,例如浏览器端。

 

为什么使用 process.nextTick() 

 

有两大原因:

 

允许用户处理错误,清理不需要的资源,或许在事件循环结束前再次尝试发送请求;

必须让回调函数在调用栈已经清除(unwound)后并且事件循环继续下去之前执行;

 

下面的两个例子都是类似的,即在 line1 派发事件,却在 line2 才添加监听,因此监听的回调是不可能被执行到的。

于是可以用 process.nextTick() 使得当前调用栈先执行完毕,也即先执行 line2 注册事件监听,然后在 nextTick 派发事件。

const EventEmitter = require(&#39;events&#39;);
const util = require(&#39;util&#39;);

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function () {
    this.emit(&#39;event&#39;);
  }.bind(this));
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on(&#39;event&#39;, function() {
  console.log(&#39;an event occurred!&#39;);
});

翻译总结:

 

这篇文章写的不太简练,也可能为了有更多的受众吧,我感觉车轱辘话比较多,一个意思要说好几遍。

 

从编程应用的角度简单来说:

 

Node.js 中的事件循环大概有七八个阶段,每个阶段都有自己的队列(queue),需要等本阶段的队列处理完成后才进入其他阶段。阶段之间会互相转换,循环顺序并不是完全固定的 ,因为很多阶段是由外部的事件触发的。

 

其中比较重要的是三个:

 

定时器阶段 timers:
定时器阶段执行定时器任务(setTimeOut(), setInterval())。

轮询阶段 poll:

          轮询阶段由 I/O 事件触发,例如 'connect','data' 等。这是比较重/重要的阶段,因为大部分程序功能就是为了 I/O 数据。

          本阶段会处理定时器任务和 poll 队列中的任务,具体逻辑:


如果有 setImmediate(),终止轮询阶段并进入检查阶段去执行;

如果没有 setImmediate(),那么就查看有没有到期的定时器,有的话就回到定时器阶段执行回调函数;

处理到期的定时器任务,然后

处理队列任务,直到队列空了或者达到上限

如果队列任务没了:


检查阶段 check:

          当轮询阶段空闲并且已经有 setImmediate() 的时候,会进入检查阶段并执行。

 

比较次要但也列在表格中的两个:

 

I/O 阶段:

          本阶段处理 I/O 异常错误;

'close'事件回调:

          本阶段处理各种 'close' 事件回调;

 

关于 setTimeout(), setImmediate(), process.nextTick():

 

setTimeout()           在某个时间值过后尽快执行回调函数;

setImmediate()       一旦轮询阶段完成就执行回调函数;

process.nextTick()   在当前调用栈结束后就立即处理,这时也必然是“事件循环继续进行之前” ;

 

优先级顺序从高到低: process.nextTick() > setImmediate() > setTimeout()

注:这里只是多数情况下,即轮询阶段(I/O 回调中)。比如之前比较 setImmediate() 和 setTimeout() 的时候就区分了所处阶段/上下文。

 

 

另:

 

关于调用栈,事件循环还可以参考这篇文章:

https://blog.risingstack.com/node-js-at-scale-understanding-node-js-event-loop/

 

这篇文章里对事件任务区分了大任务(macro task) 、小任务(micro task),每个事件循环只处理一个大任务 ,但会处理完所有小任务。

这一点和前面的文章说的不同。

examples of microtasks:

process.nextTick

promises

Object.observe

examples of macrotasks:

setTimeout

setInterval

setImmediate

I/O


声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
使用Next.js(后端集成)构建多租户SaaS应用程序使用Next.js(后端集成)构建多租户SaaS应用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技术工具构建了功能性的多租户SaaS应用程序(一个Edtech应用程序),您可以做同样的事情。 首先,什么是多租户SaaS应用程序? 多租户SaaS应用程序可让您从唱歌中为多个客户提供服务

如何使用Next.js(前端集成)构建多租户SaaS应用程序如何使用Next.js(前端集成)构建多租户SaaS应用程序Apr 11, 2025 am 08:22 AM

本文展示了与许可证确保的后端的前端集成,并使用Next.js构建功能性Edtech SaaS应用程序。 前端获取用户权限以控制UI的可见性并确保API要求遵守角色库

JavaScript:探索网络语言的多功能性JavaScript:探索网络语言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是现代Web开发的核心语言,因其多样性和灵活性而广泛应用。1)前端开发:通过DOM操作和现代框架(如React、Vue.js、Angular)构建动态网页和单页面应用。2)服务器端开发:Node.js利用非阻塞I/O模型处理高并发和实时应用。3)移动和桌面应用开发:通过ReactNative和Electron实现跨平台开发,提高开发效率。

JavaScript的演变:当前的趋势和未来前景JavaScript的演变:当前的趋势和未来前景Apr 10, 2025 am 09:33 AM

JavaScript的最新趋势包括TypeScript的崛起、现代框架和库的流行以及WebAssembly的应用。未来前景涵盖更强大的类型系统、服务器端JavaScript的发展、人工智能和机器学习的扩展以及物联网和边缘计算的潜力。

神秘的JavaScript:它的作用以及为什么重要神秘的JavaScript:它的作用以及为什么重要Apr 09, 2025 am 12:07 AM

JavaScript是现代Web开发的基石,它的主要功能包括事件驱动编程、动态内容生成和异步编程。1)事件驱动编程允许网页根据用户操作动态变化。2)动态内容生成使得页面内容可以根据条件调整。3)异步编程确保用户界面不被阻塞。JavaScript广泛应用于网页交互、单页面应用和服务器端开发,极大地提升了用户体验和跨平台开发的灵活性。

Python还是JavaScript更好?Python还是JavaScript更好?Apr 06, 2025 am 12:14 AM

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。 1.Python以简洁语法和丰富库生态着称,适用于数据分析和Web开发。 2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

如何安装JavaScript?如何安装JavaScript?Apr 05, 2025 am 12:16 AM

JavaScript不需要安装,因为它已内置于现代浏览器中。你只需文本编辑器和浏览器即可开始使用。1)在浏览器环境中,通过标签嵌入HTML文件中运行。2)在Node.js环境中,下载并安装Node.js后,通过命令行运行JavaScript文件。

在Quartz中如何在任务开始前发送通知?在Quartz中如何在任务开始前发送通知?Apr 04, 2025 pm 09:24 PM

如何在Quartz中提前发送任务通知在使用Quartz定时器进行任务调度时,任务的执行时间是由cron表达式设定的。现�...

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解锁Myrise中的所有内容
3 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

MinGW - 适用于 Windows 的极简 GNU

MinGW - 适用于 Windows 的极简 GNU

这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )专业的PHP集成开发工具

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

SublimeText3 英文版

SublimeText3 英文版

推荐:为Win版本,支持代码提示!

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境