目錄
什麼是事件循環
事件循環機制解析
階段概述
階段的詳細概述
定時器
挂起的回调函数
轮询
检查阶段
关闭的回调函数
setImmediate() 对比 setTimeout()
process.nextTick()
理解 process.nextTick()
为什么会允许这样?
process.nextTick() 对比 setImmediate()
为什么要使用 process.nextTick()?
首頁 web前端 js教程 了解Node的事件循環、process.nextTick()

了解Node的事件循環、process.nextTick()

Mar 11, 2022 pm 08:13 PM
node.js 事件循環 定時器

這篇文章給大家了解一下Nodejs中的事件循環,解析下事件循環機制、process.nextTick()等,希望對大家有幫助!

了解Node的事件循環、process.nextTick()

什麼是事件循環

事件循環是Node.js 處理非阻塞I/O 操作的機制-儘管JavaScript 是單一執行緒處理的— —當有可能的時候,它們會把操作轉移到系統核心中去。

既然目前大多數核心都是多執行緒的,它們可在背景處理多種操作。當其中的一個操作完成的時候,核心通知 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 實際上使用以上的某些步驟。

階段概述

  • 計時器:本階段執行已經被setTimeout()setInterval() 的調度回呼函數。
  • 待定回呼:執行延遲到下一個循環迭代的 I/O 回呼。
  • idle, prepare:僅系統內部使用。
  • 輪詢:檢索新的I/O 事件;執行與I/O 相關的回呼(幾乎所有情況下,除了關閉的回調函數,那些由計時器和setImmediate() 調度的之外),其餘情況node 將在適當的時候在此阻塞。
  • 偵測setImmediate() 回呼函數在這裡執行。
  • 關閉的回呼函數:一些關閉的回呼函數,如:socket.on('close', ...)

在每次執行的事件循環之間,Node.js 檢查它是否在等待任何非同步 I/O 或計時器,如果沒有的話,則完全關閉。

階段的詳細概述

定時器

#計時器指定可以執行所提供回調的閾值,而不是用戶希望其執行的確切時間。在指定的一段時間間隔後, 計時器回呼將會盡可能提早運作。但是,作業系統調度或其它正在運行的回調可能會延遲它們。

注意輪詢 階段 控制何時計時器執行。

例如,假設您調度了一個在 100 毫秒後逾時的定時器,然後您的腳本開始非同步讀取會耗費 95 毫秒的檔案:

const fs = require(&#39;fs&#39;);

function someAsyncOperation(callback) {
  // Assume this takes 95ms to complete
  fs.readFile(&#39;/path/to/file&#39;, 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() 尚未完成),因此它将等待剩下的毫秒数,直到达到最快的一个计时器阈值为止。当它等待 95 毫秒过后时,fs.readFile() 完成读取文件,它的那个需要 10 毫秒才能完成的回调,将被添加到 轮询 队列中并执行。当回调完成时,队列中不再有回调,因此事件循环机制将查看最快到达阈值的计时器,然后将回到 计时器 阶段,以执行定时器的回调。在本示例中,您将看到调度计时器到它的回调被执行之间的总延迟将为 105 毫秒。

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

挂起的回调函数

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

轮询

轮询 阶段有两个重要的功能:

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

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

当事件循环进入 轮询 阶段且_没有被调度的计时器时_,将发生以下两种情况之一:

  • 如果 轮询 队列 不是空的

    ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。

  • 如果 轮询 队列 是空的,还有两件事发生:

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

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

一旦 轮询 队列为空,事件循环将检查 _已达到时间阈值的计时器_。如果一个或多个计时器已准备就绪,则事件循环将绕回计时器阶段以执行这些计时器的回调。

检查阶段

此阶段允许人员在轮询阶段完成后立即执行回调。如果轮询阶段变为空闲状态,并且脚本使用 setImmediate() 后被排列在队列中,则事件循环可能继续到 检查 阶段而不是等待。

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

通常,在执行代码时,事件循环最终会命中轮询阶段,在那等待传入连接、请求等。但是,如果回调已使用 setImmediate()调度过,并且轮询阶段变为空闲状态,则它将结束此阶段,并继续到检查阶段而不是继续等待轮询事件。

关闭的回调函数

如果套接字或处理函数突然关闭(例如 socket.destroy()),则&#39;close&#39; 事件将在这个阶段发出。否则它将通过 process.nextTick() 发出。

setImmediate() 对比 setTimeout()

setImmediate()setTimeout() 很类似,但是基于被调用的时机,他们也有不同表现。

  • setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本。
  • setTimeout() 在最小阈值(ms 单位)过后运行脚本。

执行计时器的顺序将根据调用它们的上下文而异。如果二者都从主模块内调用,则计时器将受进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。

例如,如果运行以下不在 I/O 周期(即主模块)内的脚本,则执行两个计时器的顺序是非确定性的,因为它受进程性能的约束:

// timeout_vs_immediate.js
setTimeout(() => {
  console.log(&#39;timeout&#39;);
}, 0);

setImmediate(() => {
  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.js
const 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 的一部分。这是因为 process.nextTick() 从技术上讲不是事件循环的一部分。相反,它都将在当前操作完成后处理 nextTickQueue, 而不管事件循环的当前阶段如何。这里的一个_操作_被视作为一个从底层 C/C++ 处理器开始过渡,并且处理需要执行的 JavaScript 代码。

回顾我们的图示,任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前解析。这可能会造成一些糟糕的情况,因为它允许您通过递归 process.nextTick()调用来“饿死”您的 I/O,阻止事件循环到达 轮询 阶段。

为什么会允许这样?

为什么这样的事情会包含在 Node.js 中?它的一部分是一个设计理念,其中 API 应该始终是异步的,即使它不必是。以此代码段为例:

function apiCall(arg, callback) {
  if (typeof arg !== &#39;string&#39;)
    return process.nextTick(
      callback,
      new TypeError(&#39;argument should be string&#39;)
    );
}
登入後複製

代码段进行参数检查。如果不正确,则会将错误传递给回调函数。最近对 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&#39;t been assigned any value
  console.log(&#39;bar&#39;, bar); // undefined
});

bar = 1;
登入後複製

用户将 someAsyncApiCall() 定义为具有异步签名,但实际上它是同步运行的。当调用它时,提供给 someAsyncApiCall() 的回调是在事件循环的同一阶段内被调用,因为 someAsyncApiCall() 实际上并没有异步执行任何事情。结果,回调函数在尝试引用 bar,但作用域中可能还没有该变量,因为脚本尚未运行完成。

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

let bar;

function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log(&#39;bar&#39;, bar); // 1
});

bar = 1;
登入後複製

这又是另外一个真实的例子:

const server = net.createServer(() => {}).listen(8080);

server.on(&#39;listening&#39;, () => {});
登入後複製

只有传递端口时,端口才会立即被绑定。因此,可以立即调用 &#39;listening&#39; 回调。问题是 .on(&#39;listening&#39;) 的回调在那个时间点尚未被设置。

为了绕过这个问题,&#39;listening&#39; 事件被排在 nextTick() 中,以允许脚本运行完成。这让用户设置所想设置的任何事件处理器。

process.nextTick() 对比 setImmediate()

就用户而言,我们有两个类似的调用,但它们的名称令人费解。

  • process.nextTick() 在同一个阶段立即执行。
  • setImmediate() 在事件循环的接下来的迭代或 'tick' 上触发。

实质上,这两个名称应该交换,因为 process.nextTick()setImmediate() 触发得更快,但这是过去遗留问题,因此不太可能改变。如果贸然进行名称交换,将破坏 npm 上的大部分软件包。每天都有更多新的模块在增加,这意味着我们要多等待每一天,则更多潜在破坏会发生。尽管这些名称使人感到困惑,但它们本身名字不会改变。

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

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

有两个主要原因:

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

  2. 有时有让回调在栈展开后,但在事件循环继续之前运行的必要。

以下是一个符合用户预期的简单示例:

const server = net.createServer();
server.on(&#39;connection&#39;, (conn) => {});

server.listen(8080);
server.on(&#39;listening&#39;, () => {});
登入後複製

假设 listen() 在事件循环开始时运行,但 listening 的回调被放置在 setImmediate() 中。除非传递过主机名,才会立即绑定到端口。为使事件循环继续进行,它必须命中 轮询 阶段,这意味着有可能已经接收了一个连接,并在侦听事件之前触发了连接事件。

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

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

function MyEmitter() {
  EventEmitter.call(this);
  this.emit(&#39;event&#39;);
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on(&#39;event&#39;, () => {
  console.log(&#39;an event occurred!&#39;);
});
登入後複製

你不能立即从构造函数中触发事件,因为脚本尚未处理到用户为该事件分配回调函数的地方。因此,在构造函数本身中可以使用 process.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(() => {
    this.emit(&#39;event&#39;);
  });
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on(&#39;event&#39;, () => {
  console.log(&#39;an event occurred!&#39;);
});
登入後複製

来源:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

更多node相关知识,请访问:nodejs 教程

以上是了解Node的事件循環、process.nextTick()的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱門文章

<🎜>:泡泡膠模擬器無窮大 - 如何獲取和使用皇家鑰匙
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
北端:融合系統,解釋
3 週前 By 尊渡假赌尊渡假赌尊渡假赌
Mandragora:巫婆樹的耳語 - 如何解鎖抓鉤
3 週前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

熱門話題

Java教學
1666
14
CakePHP 教程
1425
52
Laravel 教程
1325
25
PHP教程
1273
29
C# 教程
1252
24
圖文詳解Node V8引擎的記憶體和GC 圖文詳解Node V8引擎的記憶體和GC Mar 29, 2023 pm 06:02 PM

這篇文章帶大家深入了解NodeJS V8引擎的記憶體和垃圾回收器(GC),希望對大家有幫助!

一文聊聊Node中的記憶體控制 一文聊聊Node中的記憶體控制 Apr 26, 2023 pm 05:37 PM

基於無阻塞、事件驅動建立的Node服務,具有記憶體消耗低的優點,非常適合處理海量的網路請求。在海量請求的前提下,就需要考慮「記憶體控制」的相關問題了。 1. V8的垃圾回收機制與記憶體限制 Js由垃圾回收機

深入聊聊Node中的File模組 深入聊聊Node中的File模組 Apr 24, 2023 pm 05:49 PM

文件模組是對底層文件操作的封裝,例如文件讀寫/打開關閉/刪除添加等等文件模組最大的特點就是所有的方法都提供的**同步**和**異步**兩個版本,具有sync 字尾的方法都是同步方法,沒有的都是異

一起聊聊Node中的事件循環 一起聊聊Node中的事件循環 Apr 11, 2023 pm 07:08 PM

事件循環是 Node.js 的基本組成部分,透過確保主執行緒不被阻塞來實現非同步編程,了解事件循環對建立高效應用程式至關重要。以下這篇文章就來帶大家深入了解Node中的事件循環 ,希望對大家有幫助!

node無法用npm指令怎麼辦 node無法用npm指令怎麼辦 Feb 08, 2023 am 10:09 AM

node無法用npm指令是因為沒有正確配置環境變量,其解決方法是:1、開啟“系統屬性”;2、找到“環境變數”->“系統變數”,然後編輯環境變數;3、找到nodejs所在的資料夾;4、點選「確定」即可。

深入了解Node中的Buffer 深入了解Node中的Buffer Apr 25, 2023 pm 07:49 PM

一開始的時候 JS 只在瀏覽器端運行,對於 Unicode 編碼的字串容易處理,但對於二進位和非 Unicode 編碼的字串處理困難。並且二進制是電腦最底層的資料格式,視訊/音訊/程式/網路包

如何在 iPhone 相機上設定定時器 如何在 iPhone 相機上設定定時器 Apr 14, 2023 am 10:43 AM

您可以在 iPhone 相機上設定多長時間的定時器?當您在 iPhone 的相機應用程式中存取定時器選項時,您將獲得在兩種模式之間進行選擇的選項:3 秒 (3s)和10 秒 (10s)。當您手持 iPhone 時,您可以使用第一個選項從前置或後置相機快速自拍。第二個選項在場景中很有用,可以在遠處將 iPhone 安裝到三腳架上來點擊合照或自拍。如何在 iPhone 相機上設定定時器 雖然在 iPhone 相機上設定定時器是一個相當簡單的過程,但具體操作方式因所使用的 iPhone 機型而異。

如何實作Workerman文件中的定時器功能 如何實作Workerman文件中的定時器功能 Nov 08, 2023 pm 05:06 PM

如何實現Workerman文件中的定時器功能Workerman是一款強大的PHP非同步網路通訊框架,它提供了豐富的功能,其中就包括定時器功能。使用定時器可以在指定的時間間隔內執行程式碼,非常適合定時任務、輪詢等應用程式場景。接下來,我將詳細介紹如何在Workerman中實現定時器功能,並提供具體的程式碼範例。第一步:安裝Workerman首先,我們需要安裝Worker

See all articles