首頁  >  文章  >  web前端  >  深入解析 Node.js 的回呼隊列

深入解析 Node.js 的回呼隊列

青灯夜游
青灯夜游轉載
2020-09-01 10:39:061561瀏覽

深入解析 Node.js 的回呼隊列

佇列是 Node.js 中用於有效處理非同步操作的重要技術。 【影片教學推薦:node js教學 】

在本文中,我們將深入研究Node.js 中的佇列:它們是什麼,它們如何運作(透過事件循環)以及它們的類型。

Node.js 中的佇列是什麼?

佇列是 Node.js 中用於組織非同步操作的資料結構。這些操作以不同的形式存在,包括HTTP請求、讀取或寫入檔案操作、流等。

在 Node.js 中處理非同步操作非常具有挑戰性。

HTTP 請求期間可能會出現不可預測的延遲(或更糟糕的可能性是沒有結果),這取決於網路品質。嘗試用 Node.js 讀寫檔案時也有可能會產生延遲,這取決於檔案的大小。

類似於計時器和其他的許多操作,非同步操作完成的時間也有可能是不確定的。

在這些不同的延遲情況之下,Node.js 需要能夠有效地處理所有這些操作。

Node.js 無法處理基於 first-start-first-handle (先開始先處理)或 first-finish-first-handle (先結束先處理)的操作。

之所以不能這樣做的一個原因是,在一個非同步操作中可能還會包含另一個非同步操作。

為第一個非同步過程留出空間意味著必須先完成內部非同步過程,然後才能考慮佇列中的其他非同步操作。

有許多情況需要考慮,因此最好的選擇是製定規則。這個規則影響了事件循環和佇列在 Node.js 中的工作方式。

讓我們簡單地看一下 Node.js 是怎麼處理非同步操作的。

呼叫堆疊,事件循環和回調佇列

呼叫堆疊被用來追蹤目前正在執行的函數以及從何處開始運行。當一個函數將要執行時,它會被加入到呼叫堆疊中。這有助於 JavaScript 在執行函數後重新追蹤其處理步驟。

回呼佇列是在後台操作完成時把回呼函數儲存為非同步操作的佇列。它們以先進先出(FIFO)的方式工作。我們將會在本文後面介紹不同類型的回呼隊列。

請注意,Node.js 負責所有非同步活動,因為 JavaScript 可以利用其單線程性質來阻止產生新的線程。

在完成後台操作後,它還負責在回調佇列中新增函數。 JavaScript 本身與回呼佇列無關。同時事件循環會連續檢查呼叫堆疊是否為空,以便可以從回調佇列中提取一個函數並加入到呼叫堆疊中。事件循環僅在執行所有同步操作之後才檢查佇列。

那麼,事件循環是按照什麼樣的順序從佇列中選擇回呼函數的呢?

首先,讓我們來看看回呼隊列的五種主要類型。

回呼佇列的類型

IO 佇列(IO queue)

IO操作是指涉及外部裝置(如電腦的硬碟、網卡等)的操作。常見的操作包括讀寫檔案操作、網路操作等。這些操作應該是異步的,因為它們留給 Node.js 處理。

JavaScript 無法存取電腦的內部裝置。當執行此類操作時,JavaScript 會將其傳輸到 Node.js 以在背景處理。

完成後,它們將會被轉移到 IO 回呼佇列中,來進行事件循環,以轉移到呼叫堆疊中執行。

計時器佇列(Timer queue)

每個涉及Node.js 計時器功能的操作(如setTimeout()setInterval())都是要被加入到計時器佇列的。

請注意,JavaScript 語言本身沒有計時器功能。它使用 Node.js 提供的計時器 API(包括 setTimeout )執行與時間相關的操作。所以計時器操作是異步的。無論是 2 秒還是 0 秒,JavaScript 都會把與時間相關的操作交給 Node.js,然後將其完成並加入到計時器佇列中。

例如:

setTimeout(function() {
        console.log('setTimeout');
    }, 0)
    console.log('yeah')


# 返回
yeah
setTimeout

在處理非同步作業時,JavaScript 會繼續執行其他動作。只有在所有同步操作都已處理完畢後,事件循環才會進入回呼佇列。

微任務佇列(Microtask queue)

此佇列分為兩個佇列:

  • 第一個佇列包含因process.nextTick 函數而延遲的函數。

事件循環執行的每個迭代稱為一個 tick(時間刻度)。

process.nextTick 是一个函数,它在下一个 tick (即事件循环的下一个迭代)执行一个函数。微任务队列需要存储此类函数,以便可以在下一个 tick 执行它们。

这意味着事件循环必须继续检查微任务队列中的此类函数,然后再进入其他队列。

  • 第二个队列包含因 promises 而延迟的函数。

如你所见,在 IO 和计时器队列中,所有与异步操作有关的内容都被移交给了异步函数。

但是 promise 不同。在 promise 中,初始变量存储在 JavaScript 内存中(你可能已经注意到了fcfd9c63d2b5ae1697cab5937aad0a2f)。

异步操作完成后,Node.js 会将函数(附加到 Promise)放在微任务队列中。同时它用得到的结果来更新 JavaScript 内存中的变量,以使该函数不与 fcfd9c63d2b5ae1697cab5937aad0a2f 一起运行。

以下代码说明了 promise 是如何工作的:

let prom = new Promise(function (resolve, reject) {
        // 延迟执行
        setTimeout(function () {
            return resolve("hello");
        }, 2000);
    });
    console.log(prom);
    // Promise { <pending> }
    
    prom.then(function (response) {
        console.log(response);
    });
    // 在 2000ms 之后,输出
    // hello

关于微任务队列,需要注意一个重要功能,事件循环在进入其他队列之前要反复检查并执行微任务队列中的函数。例如,当微任务队列完成时,或者说计时器操作执行了 Promise 操作,事件循环将会在继续进入计时器队列中的其他函数之前参与该 Promise 操作。

因此,微任务队列比其他队列具有最高的优先级。

检查队列(Check queue)

检查队列也称为即时队列(immediate queue)。IO 队列中的所有回调函数均已执行完毕后,立即执行此队列中的回调函数。setImmediate 用于向该队列添加函数。

例如:

const fs = require(&#39;fs&#39;);
setImmediate(function() {
    console.log(&#39;setImmediate&#39;);
})
// 假设此操作需要 1ms
fs.readFile(&#39;path-to-file&#39;, function() {
    console.log(&#39;readFile&#39;)
})
// 假设此操作需要 3ms
do...while...

执行该程序时,Node.js 把 setImmediate 回调函数添加到检查队列。由于整个程序尚未准备完毕,因此事件循环不会检查任何队列。

因为 readFile 操作是异步的,所以会移交给 Node.js,之后程序将会继续执行。

do while  操作持续 3ms。在这段时间内,readFile 操作完成并被推送到 IO 队列。完成此操作后,事件循环将会开始检查队列。

尽管首先填充了检查队列,但只有在 IO 队列为空之后才考虑使用它。所以在 setImmediate 之前,将 readFile 输出到控制台。

关闭队列(Close queue)

此队列存储与关闭事件操作关联的函数。

包括以下内容:

这些队列被认为是优先级最低的,因为此处的操作会在以后发生。

你肯sing不希望在处理 promise 函数之前在 close 事件中执行回调函数。当服务器已经关闭时,promise 函数会做些什么呢?

队列顺序

微任务队列具有最高优先级,其次是计时器队列,I/O队列,检查队列,最后是关闭队列。

回调队列的例子

让我们通过一个更复杂的例子来说明队列的类型和顺序:

const fs = require("fs");

// 假设此操作需要 2ms
fs.writeFile(&#39;./new-file.json&#39;, &#39;...&#39;, function() {
    console.log(&#39;writeFile&#39;)
})

// 假设这需要 10ms 才能完成 
fs.readFile("./file.json", function(err, data) {
    console.log("readFile");
});

// 不需要假设,这实际上需要 1ms
setTimeout(function() {
    console.log("setTimeout");
}, 1000);

// 假设此操作需要 3ms
while(...) {
    ...
}

setImmediate(function() {
    console.log("setImmediate");
});

// 解决 promise 需要 4 ms
let promise = new Promise(function (resolve, reject) {
    setTimeout(function () {
        return resolve("promise");
    }, 4000);
});
promise.then(function(response) {
    console.log(response)
})

console.log("last line");

程序流程如下:

  • 在 0 毫秒时,程序开始。
  • 在 Node.js 将回调函数添加到 IO 队列之前,fs.writeFile 在后台花费 2 毫秒。

fs.readFile takes 10ms at the background before Node.js adds the callback function to the IO queue.

  • 在 Node.js 将回调函数添加到 IO 队列之前,fs.readFile 在后台花费 10 毫秒。
  • 在 Node.js 将回调函数添加到计时器队列之前,setTimeout 在后台花费 1ms。
  • 现在,while 操作(同步)需要 3ms。在此期间,线程被阻止(请记住 JavaScript 是单线程的)。
  • 同样在这段时间内,setTimeoutfs.writeFile 操作完成,并将它们的回调函数分别添加到计时器和 IO 队列中。

现在的队列是:

// queues
Timer = [
    function () {
        console.log("setTimeout");
    },
];
IO = [
    function () {
        console.log("writeFile");
    },
];

setImmediate 将回调函数添加到 Check 队列中:

js
// 队列
Timer...
IO...
Check = [
    function() {console.log("setImmediate")}
]

在将 promise 操作添加到微任务队列之前,需要花费 4ms 的时间在后台进行解析。

最后一行是同步的,因此将会立即执行:

# 返回
"last line"

因为所有同步活动都已完成,所以事件循环开始检查队列。由于微任务队列为空,因此它从计时器队列开始:

// 队列
Timer = [] // 现在是空的
IO...
Check...


# 返回
"last line"
"setTimeout"

当事件循环继续执行队列中的回调函数时,promise 操作完成并被添加到微任务队列中:

// 队列
    Timer = [];
    Microtask = [
        function (response) {
            console.log(response);
        },
    ];
    IO = []; // 当前是空的
    Check = []; // 当前是在 IO 的后面,为空


    # results
    "last line"
    "setTimeout"
    "writeFile"
    "setImmediate"

几秒钟后,readFile 操作完成,并添加到 IO 队列中:

// 队列
    Timer = [];
    Microtask = []; // 当前是空的
    IO = [
        function () {
            console.log("readFile");
        },
    ];
    Check = [];


    # results
    "last line"
    "setTimeout"
    "writeFile"
    "setImmediate"
    "promise"

最后,执行所有回调函数:

// 队列
    Timer = []
    Microtask = []
    IO = [] // 现在又是空的
    Check = [];


    # results
    "last line"
    "setTimeout"
    "writeFile"
    "setImmediate"
    "promise"
    "readFile"

这里要注意的三点:

  • 异步操作取决于添加到队列之前的延迟时间。并不取决于它们在程序中的存放顺序。
  • 事件循环在每次迭代之继续检查其他任务之前,会连续检查微任务队列。
  • 即使在后台有另一个 IO 操作(readFile),事件循环也会执行检查队列中的函数。这样做的原因是此时 IO 队列为空。请记住,在执行 IO 队列中的所有的函数之后,将会立即运行检查队列回调。

总结

JavaScript 是单线程的。每个异步函数都由依赖操作系统内部函数工作的 Node.js 去处理。

Node.js 负责将回调函数(通过 JavaScript 附加到异步操作)添加到回调队列中。事件循环会确定将要在每次迭代中接下来要执行的回调函数。

了解队列如何在 Node.js 中工作,使你对其有了更好的了解,因为队列是环境的核心功能之一。 Node.js 最受欢迎的定义是 non-blocking(非阻塞),这意味着异步操作可以被正确的处理。都是因为有了事件循环和回调队列才能使此功能生效。

更多编程相关知识,可访问:编程教学!!

以上是深入解析 Node.js 的回呼隊列的詳細內容。更多資訊請關注PHP中文網其他相關文章!

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