首頁  >  文章  >  web前端  >  什麼是事件循環?詳解Node.js中的事件循環

什麼是事件循環?詳解Node.js中的事件循環

青灯夜游
青灯夜游轉載
2022-03-25 20:32:571785瀏覽

什麼是事件循環?本篇文章為大家介紹一下Node中的事件循環,希望對大家有幫助!

什麼是事件循環?詳解Node.js中的事件循環

什麼是事件循環?

儘管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的佇列callback 執行,然而每一個階段基於它自己的方式執行,總體來講,當事件循環進入到一個階段裡,它將執行當前階段的任何操作,開始執行目前階段佇列中的回呼直到佇列完全消耗完或執行到佇列的最大資料。當佇列消耗完或達到最大數量,事件循環就會移動到下一個階段。

階段概述

  • #timers 這個階段執行setTimeout() 和setInterval() 的回呼
  • pending callbacks 執行I/O 回呼延後到下一個迴圈迭代
  • idle,prepare 僅在內部使用
  • #poll 檢索新的I/O 事件;執行I/O 相關的回調(幾乎所有相關的回調,關閉回調,)
  • check setImmediate() 會在此階段呼叫
  • close callbacks 關閉回呼,例如: socket.on('close', ...)

在事件循環的每個過程中,Node.js檢查是否它正在等待非同步的I/O和計時器,如果沒有則完全關閉

階段詳情

timer

一個計時器指定一個回呼會被執行的臨界點,而不是人們想讓它執行的時間,計時器會在指定的過去時間之後盡可能早的執行,然而,作業系統調度或其他回調會讓它延遲執行。

從技術角度上講,poll 階段決定了回呼何時執行

例如,你設定了一個計時器,100 ms之後執行,然而你的腳本非同步讀取了一個文件花了95ms

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
  }
});

當事件循環進入了poll 階段,是一個空的佇列,(fs.readFile() 還沒有完成),因此它會等待剩餘的毫秒數直到最快的計時器閾值到達,當95 ms之後,fs.readFile() 完成了讀取檔案並且會花費10 ms完成添加到poll 階段並且執行完畢,當回調完成,隊列中沒有回調要執行了,事件循環循環返回到timers 階段,執行計時器的回呼。在這個例子中,你會看到計時器被延遲了105 ms之後執行

為了防止poll 階段阻塞事件循環,libuv(實現了事件循環和平台上所有的異步行為的C語言庫)在poll 階段同樣也有一個最大值停止輪訓更多事件

pending callbacks

此階段為某些系統操作(例如TCP 錯誤類型)執行回呼。例如,如果 TCP 套接字在嘗試連線時收到 ECONNREFUSED,則某些 *nix 系統希望等待報告錯誤。這將在掛起的回調階段排隊執行。

poll

poll 階段有兩個主要的功能

  1. 計算I/O 阻斷的時間
  2. 執行poll 佇列中的事件

當事件循環進入到了poll階段且沒有計時器,發生下列兩種事情

  • 如果poll 佇列中不為空,則事件循環會同步地迭代執行每個回調直到執行所有,或達到系統的硬限制
  • 如果poll 佇列是空的,以下兩種情況會發生
    • 如果是setImmediate的回調,事件循環會結束poll 階段並進入到check 階段執行回呼
    • 如果不是setImmediate,事件循環會等待回調加入到佇列中,然後立即執行

一旦poll 佇列是空的,事件循環會偵測計時器是否到時間,如果有,事件循環會到達timers 階段執行計時器回呼

#

check

此阶段允许人们在 poll 阶段完成后立即执行回调。 如果轮询阶段变得空闲并且脚本已使用 setImmediate() 排队,则事件循环可能会继续到 check 阶段而不是等待。

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

通常,随着代码的执行,事件循环最终会到达 poll 阶段,它将等待传入的连接、请求等。但是,如果使用 setImmediate() 安排了回调并且 poll 阶段变得空闲,它将结束并继续 check 阶段,而不是等待 poll 事件。

close callbacks

如果一个 socket 或者操作突然被关闭(e.g socket.destroy()),close 事件会被发送到这个阶段,否则会通过process.nextTick()发送

setImmediate() VS setTimeout()

setImmediate() 和 setTimeout() 是相似的,但是不同的行为取决于在什么时候被调用

  • setTimmediate() 在 poll 阶段一旦执行完就会执行
  • setTimeout() 在一小段时间过去之后被执行

每个回调执行的顺序依赖他们被调用的上下本环境,如果在同一个模块被同时调用,那么时间会受到进程性能的限制(这也会被运行在这台机器的其他应用所影响)

例如,如果我们不在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 循环中,immediate 回调总是会先执行

// 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() 是异步API的一部分,但是你可能已经注意到了它没有出现在图表中,这是因为 process.nextTick() 不是事件循环技术的一部分,相反,当前操作执行完毕之后 nextTickQueue 会被执行,无论事件循环的当前阶段如何。 在这里,操作被定义为来自底层 C/C++ 处理程序的转换,并处理需要执行的 JavaScript。 根据图表,你可以在任意阶段调用 process.nextTick(),在事件循环继续执行之前,所有传递给 process.nextTick() 的回调都将被执行,这个会导致一些坏的情况因为它允许你递归调用 process.nextTick() "starve" 你的 I/O ,这会阻止事件循环进入 poll 阶段。

为什么这会被允许

为什么这种情况会被包含在Node.js中?因为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 开始超出最大调用堆栈大小。

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

以上是什麼是事件循環?詳解Node.js中的事件循環的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除