nodejs的事件循环,node和浏览器对事件循环的处理

  nodejs的事件循环,node和浏览器对事件循环的处理

  本文告诉你Nodejs中的事件循环,分析事件循环机制,process.nextTick()等。希望对你有帮助!

  node.js速度课程简介:进入学习

  

什么是事件循环

  事件循环是Node.js处理非阻塞I/O操作的机制。虽然JavaScript是单线程处理的,但是他们会在可能的时候把操作转移到系统内核。

  由于目前大多数内核都是多线程的,所以可以在后台处理各种操作。当其中一个操作完成时,内核通知Node.js将适当的回调函数添加到轮询队列中,并等待执行的机会。我们将在本文后面详细介绍。

  

事件循环机制解析

   node . js启动时,会初始化事件循环,并处理提供的输入脚本(或者扔进REPL,不在本文讨论范围内)。它可能会调用一些异步API,调度定时器,或者调用process.nextTick(),然后开始处理事件循环。

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

  计时器

   待定的回调

   闲着,准备

  来袭:

   民意测验的人脉,

  数据等。

   检查

  关闭回调

  ————————————————————————————————————注:每个盒子被称为事件循环机制的一个阶段。

  每个阶段都有一个FIFO队列来执行回调。虽然每个阶段都是特殊的,但一般来说,当事件循环进入给定阶段时,它会执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列耗尽或执行了最大数量的回调。当队列耗尽或达到回调限制时,事件循环将移动到下一个阶段,以此类推。

  由于这些操作中的任何一个都可以调度由内核安排的在轮询阶段中处理的_ more _ operations和新事件,所以在处理轮询事件时可以将轮询事件排队。因此,长时间运行的回调可以允许轮询阶段运行的时间长于计时器的阈值时间。详见第计时器节和第轮询节。

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

  

阶段概述

  定时器:此阶段执行已由setTimeout()和setInterval()设置的调度回调函数。待定回调:执行I/O回调被推迟到下一次循环迭代。idle, prepare:仅供内部使用。轮询:检索新的I/O事件;执行与I/O相关的回调(在几乎所有情况下,除了关闭的回调函数,那些由timer和setImmediate()调度的回调函数),其余的cases节点将在适当的时间阻塞在这里。检测:这里执行setimmediate()回调函数。关闭的回调函数:一些封闭的回调函数如:socket.on(close ),).在每个运行的事件循环之间,Node.js检查它是否在等待任何异步I/O或定时器,如果不是,它就完全关闭。

  

阶段的详细概述

  

定时器

   Timer指定可以执行提供的回调的阈值,而不是用户希望它执行的确切时间。在指定的时间间隔后,计时器回调将尽快运行。但是,操作系统调度或其他正在运行的回调可能会延迟它们。

  注意:轮询阶段控制计时器的执行时间。

  例如,假设您安排一个计时器在100毫秒后超时,然后您的脚本开始异步读取需要95毫秒的文件:

  const fs=require( fs );

  函数someAsyncOperation(回调){

  //假设这需要95毫秒才能完成

  fs.readFile(/path/to/file ,回调);

  }

  const time out scheduled=date . now();

  setTimeout(()={

  const delay=date . now()-time out scheduled;

  console . log(` { delay }毫秒后,我已被安排`);

  }, 100);

  //执行需要95毫秒才能完成的someAsyncOperation

  someasyncooperation(()={

  const start callback=date . now();

  //做一些需要10毫秒的事情.

  while(date . now()-start callback 10){

  //什么都不做

  }

  });当事件循环进入轮询阶段时,它有一个空队列(此时fs.readFile()尚未完成),所以它将等待剩余的毫秒,直到达到最快的计时器阈值。当它等待95毫秒时,fs.readFile()完成读取文件,它的回调(需要10毫秒完成)将被添加到轮询队列中并被执行。当回调完成后,队列中不再有回调,所以事件循环机制会检查最快达到阈值的计时器,然后它会返回到计时器阶段执行计时器的回调。在本例中,您将看到调度计时器和执行回调之间的总延迟是105毫秒。

  注意:为了防止轮询阶段的饥饿事件循环,libuv(实现Node.js事件循环和平台所有异步行为的C函数库)在停止轮询获取更多事件之前有一个硬最大值(取决于系统)。

  

挂起的回调函数

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

  

轮询

  轮询载物台有两个重要功能:

  应该计算阻塞和轮询I/O的时间。

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

  当事件周期进入轮询阶段且_没有预定定时器_时,将发生以下两种情况之一:

  如果轮询队列不是空的

  ,事件循环将遍历回调队列并同步执行它们,直到队列耗尽或达到与系统相关的硬限制。

  如果轮询排队是空的,还会发生两件事:

  如果脚本由setImmediate()调度,事件循环将结束轮询阶段,并继续检查阶段以执行那些调度的脚本。

  如果脚本未被setImmediate()被调度,事件循环将等待回调被添加到队列中,然后立即执行。

  一旦轮询队列为空,事件循环将检查已经达到时间阈值的计时器。如果一个或多个定时器准备好了,事件循环将返回到定时器阶段来执行这些定时器的回调。

  

检查阶段

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

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

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

  

关闭的回调函数

  如果套接字或处理程序突然关闭(如socket.destroy()),则在此阶段将发出“close”事件。否则将通过process.nextTick()发出。

  

setImmediate() 对比 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

  超时

  马上

  $ node timeout_vs_immediate.js

  马上

  超时但是,如果在I/O循环中调用这两个函数,setImmediate总是首先被调用:

  //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

  马上

  超时

  $ node timeout_vs_immediate.js

  马上

  与setTimeout()相比,使用setImmediate()进行超时的主要优点是,如果setImmediate()在I/O周期中被调度,它将在任何计时器之前执行,而不管有多少个计时器。

  

process.nextTick()

  

理解 process.nextTick()

  您可能已经注意到,尽管process.nextTick()是异步API的一部分,但它并没有显示在图中。这是因为process.nextTick()在技术上不是事件循环的一部分。相反,它将在当前操作完成后处理nextTickQueue,而不考虑事件周期的当前阶段。这里,an _ operation _被看作是从底层C/C处理器的一个过渡,处理需要执行的JavaScript代码。

  回头看看我们的示例,每当在给定阶段调用process.nextTick()时,传递给process.nextTick()的所有回调都将在事件循环继续之前得到解析。这可能会导致一些不好的情况,因为它允许您通过递归 process.nextTick()调用来“饿死”您的 I/O会阻止事件循环到达轮询阶段。

  

为什么会允许这样?

   node . js为什么会收录这样的东西?它的一部分是一种设计哲学,其中API应该总是异步的,即使它不是必须的。以这段代码片段为例:

  函数apiCall(arg,callback) {

  if (typeof arg!==string )

  返回process.nextTick(

  回拨,

  new TypeError(“参数应为字符串”)

  );

  }用于参数检查的代码段。否则,错误将被传递给回调函数。最近更新了API,允许向process.nextTick()传递参数,这将允许它接受回调函数位置之后的任何参数,并将参数作为回调函数的参数传递给回调函数,这样就不用嵌套函数了。

  我们所做的是将错误返回给用户,但只是在执行了用户代码的其余部分之后。通过使用process.nextTick(),我们保证apiCall()总是在剩余的用户代码之后、事件循环继续之前执行其回调函数。为了实现这一点,允许JS调用栈扩展,然后立即执行提供的回调,允许递归调用process.nextTick()而不触及RangeError:超过了V8的最大调用栈大小限制。

  这种设计原则可能会导致一些潜在的问题。以这段代码片段为例:

  让吧;

  //这具有异步签名,但同步调用回调

  函数someAsyncApiCall(回调){

  回调();

  }

  //在“someAsyncApiCall”完成之前调用回调。

  someasyncalic(()={

  //因为someAsyncApiCall已经完成,所以bar没有被赋值

  console.log(bar ,bar);//未定义

  });

  bar=1;用户someAsyncApiCall()被定义为具有异步签名,但实际上它是同步运行的。调用它时,提供给someasyncapical()的回调是在事件周期的同一个阶段调用的,因为someasyncapical()实际上并不异步做任何事情。结果,回调函数试图引用bar,但是变量可能不在作用域内,因为脚本还没有运行完。

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

  让吧;

  函数someAsyncApiCall(回调){

  process.nextTick(回调);

  }

  someasyncalic(()={

  console.log(bar ,bar);//1

  });

  bar=1;这是另一个真实的例子:

  const server=net . create server(()={ })。听(8080);

  server.on(listening ,()={ });只有端口通过,端口才会立即绑定。因此,可以立即调用“监听”回调。问题是。on(listening )尚未在该时间点设置。

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

  

process.nextTick() 对比 setImmediate()

  就用户而言,我们有两个类似的电话,但是名字让人费解。

  Process.nextTick()在同一个阶段立即执行。SetImmediate()在事件循环的下一次迭代或“滴答”时触发。本质上,这两个名字应该互换,因为process.nextTick()比setImmediate()触发得更快,但这是过去的遗留问题,所以不太可能改变。如果贸然进行名称交换,npm上的大部分软件包都会被销毁。每天都有更多的新模块加入,这意味着我们每天等待的时间越长,潜在的损害就越多。虽然这些名字很混乱,但是他们自己的名字是不会变的。

  我们建议开发人员在所有情况下都使用setImmediate(),因为它更容易理解。

  

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

  有两个主要原因:

  允许用户在事件循环继续之前处理错误、清理任何不必要的资源或重试请求。

  有时,在堆栈扩展之后,事件循环继续之前,有必要运行回调。

  下面是一个满足用户期望的简单示例:

  const server=net . create server();

  server.on(连接,(连接)={ });

  server . listen(8080);

  server.on(listening ,()={ });假设listen()在事件循环的开始运行,但是listen的回调放在setImmediate()中。除非传递主机名,否则它会立即绑定到端口。为了使事件循环继续,它必须达到轮询阶段,这意味着在侦听事件之前,可能已经接收到连接并触发了连接事件。

  另一个示例运行从EventEmitter继承的函数构造函数,它希望调用该构造函数:

  const event emitter=require( events );

  const util=require( util );

  函数my mitter(){

  event emitter . call(this);

  this . emit(“event”);

  }

  util。inherits(我的mitter,事件发射器);

  const my mitter=new my mitter();

  我的密特。on( event ,()={

  console.log(发生了一个事件!);

  });你不能立即从构造函数中触发事件,因为脚本尚未处理到用户为该事件分配回调函数的地方。因此,在构造函数本身中可以使用process.nextTick()来设置回调,以便在构造函数完成后发出该事件,这是预期的结果:

  const事件发射器=require( events );

  const util=require( util );

  函数我的密特(){

  事件发射器。叫(这个);

  //一旦分配了处理程序,就使用下一滴答发出事件

  process.nextTick(()={

  这个。emit(" event ");

  });

  }

  util。inherits(我的mitter,事件发射器);

  const my mitter=new my mitter();

  我的密特。on( event ,()={

  console.log(发生了一个事件!);

  });更多结节相关知识,请访问:节点射流研究…教程!以上就是了解结节中的事件循环、process.nextTick()的详细内容,更多请关注我们其它相关文章!

郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。

留言与评论(共有 条评论)
   
验证码: