js 延迟几秒执行_深入研究 Node.js 的回调队列
隊列是 Node.js 中用于有效處理異步操作的一項重要技術。
在本文中,我們將深入研究 Node.js 中的隊列:它們是什么,它們?nèi)绾喂ぷ?#xff08;通過事件循環(huán))以及它們的類型。
Node.js 中的隊列是什么?
隊列是 Node.js 中用于組織異步操作的數(shù)據(jù)結構。這些操作以不同的形式存在,包括HTTP請求、讀取或寫入文件操作、流等。
在 Node.js 中處理異步操作非常具有挑戰(zhàn)性。
HTTP 請求期間可能會出現(xiàn)不可預測的延遲(或者更糟糕的可能性是沒有結果),具體取決于網(wǎng)絡質(zhì)量。嘗試用 Node.js 讀寫文件時也有可能會產(chǎn)生延遲,具體取決于文件的大小。
類似于計時器和其他的許多操作,異步操作完成的時間也有可能是不確定的。
在這些不同的延遲情況之下,Node.js 需要能夠有效地處理所有這些操作。
Node.js 無法處理基于 first-start-first-handle (先開始先處理)或 first-finish-first-handle (先結束先處理)的操作。
之所以不能這樣做的一個原因是,在一個異步操作中可能還會包含另一個異步操作。
為第一個異步過程留出空間意味著必須先要完成內(nèi)部異步過程,然后才能考慮隊列中的其他異步操作。
有許多情況需要考慮,因此最好的選擇是制定規(guī)則。這個規(guī)則影響了事件循環(huán)和隊列在 Node.js 中的工作方式。
讓我們簡要地看一下 Node.js 是怎樣處理異步操作的。
調(diào)用棧,事件循環(huán)和回調(diào)隊列
調(diào)用棧被用于跟蹤當前正在執(zhí)行的函數(shù)以及從何處開始運行。當一個函數(shù)將要執(zhí)行時,它會被添加到調(diào)用堆棧中。這有助于 JavaScript 在執(zhí)行函數(shù)后重新跟蹤其處理步驟。
回調(diào)隊列是在后臺操作完成時把回調(diào)函數(shù)保存為異步操作的隊列。它們以先進先出(FIFO)的方式工作。我們將會在本文后面介紹不同類型的回調(diào)隊列。
請注意,Node.js 負責所有異步活動,因為 JavaScript 可以利用其單線程性質(zhì)來阻止產(chǎn)生新的線程。
在完成后臺操作后,它還負責向回調(diào)隊列添加函數(shù)。 JavaScript 本身與回調(diào)隊列無關。同時事件循環(huán)會連續(xù)檢查調(diào)用棧是否為空,以便可以從回調(diào)隊列中提取一個函數(shù)并添加到調(diào)用棧中。事件循環(huán)僅在執(zhí)行所有同步操作之后才檢查隊列。
那么,事件循環(huán)是按照什么樣的順序從隊列中選擇回調(diào)函數(shù)的呢?
首先,讓我們看一下回調(diào)隊列的五種主要類型。
回調(diào)隊列的類型
IO 隊列(IO queue)
IO操作是指涉及外部設備(如計算機的硬盤、網(wǎng)卡等)的操作。常見的操作包括讀寫文件操作、網(wǎng)絡操作等。這些操作應該是異步的,因為它們留給 Node.js 處理。
JavaScript 無法訪問計算機的內(nèi)部設備。當執(zhí)行此類操作時,JavaScript 會將其傳輸?shù)?Node.js 以在后臺處理。
完成后,它們將會被轉移到 IO 回調(diào)隊列中,來進行事件循環(huán),以轉移到調(diào)用棧中執(zhí)行。
計時器隊列(Timer queue)
每個涉及 Node.js 計時器功能的操作(如 setTimeout() 和 setInterval())都是要被添加到計時器隊列的。
請注意,JavaScript 語言本身沒有計時器功能。它使用 Node.js 提供的計時器 API(包括 setTimeout )執(zhí)行與時間相關的操作。所以計時器操作是異步的。無論是 2 秒還是 0 秒,JavaScript 都會把與時間相關的操作移交給 Node.js,然后將其完成并添加到計時器隊列中。
例如:
setTimeout(function() {console.log('setTimeout');}, 0)console.log('yeah')# 返回 yeah setTimeout在處理異步操作時,JavaScript 會繼續(xù)執(zhí)行其他操作。只有在所有同步操作都已被處理完畢后,事件循環(huán)才會進入回調(diào)隊列。
微任務隊列(Microtask queue)
該隊列分為兩個隊列:
- 第一個隊列包含因 process.nextTick 函數(shù)而延遲的函數(shù)。
事件循環(huán)執(zhí)行的每個迭代稱為一個 tick(時間刻度)。
process.nextTick 是一個函數(shù),它在下一個 tick (即事件循環(huán)的下一個迭代)執(zhí)行一個函數(shù)。微任務隊列需要存儲此類函數(shù),以便可以在下一個 tick 執(zhí)行它們。
這意味著事件循環(huán)必須繼續(xù)檢查微任務隊列中的此類函數(shù),然后再進入其他隊列。
- 第二個隊列包含因 promises 而延遲的函數(shù)。
如你所見,在 IO 和計時器隊列中,所有與異步操作有關的內(nèi)容都被移交給了異步函數(shù)。
但是 promise 不同。在 promise 中,初始變量存儲在 JavaScript 內(nèi)存中(你可能已經(jīng)注意到了<Pending>)。
異步操作完成后,Node.js 會將函數(shù)(附加到 Promise)放在微任務隊列中。同時它用得到的結果來更新 JavaScript 內(nèi)存中的變量,以使該函數(shù)不與 <Pending> 一起運行。
以下代碼說明了 promise 是如何工作的:
let prom = new Promise(function (resolve, reject) {// 延遲執(zhí)行setTimeout(function () {return resolve("hello");}, 2000);});console.log(prom);// Promise { <pending> }prom.then(function (response) {console.log(response);});// 在 2000ms 之后,輸出// hello關于微任務隊列,需要注意一個重要功能,事件循環(huán)在進入其他隊列之前要反復檢查并執(zhí)行微任務隊列中的函數(shù)。例如,當微任務隊列完成時,或者說計時器操作執(zhí)行了 Promise 操作,事件循環(huán)將會在繼續(xù)進入計時器隊列中的其他函數(shù)之前參與該 Promise 操作。
因此,微任務隊列比其他隊列具有最高的優(yōu)先級。
檢查隊列(Check queue)
檢查隊列也稱為即時隊列(immediate queue)。IO 隊列中的所有回調(diào)函數(shù)均已執(zhí)行完畢后,立即執(zhí)行此隊列中的回調(diào)函數(shù)。setImmediate 用于向該隊列添加函數(shù)。
例如:
const fs = require('fs'); setImmediate(function() {console.log('setImmediate'); }) // 假設此操作需要 1ms fs.readFile('path-to-file', function() {console.log('readFile') }) // 假設此操作需要 3ms do...while...執(zhí)行該程序時,Node.js 把 setImmediate 回調(diào)函數(shù)添加到檢查隊列。由于整個程序尚未準備完畢,因此事件循環(huán)不會檢查任何隊列。
因為 readFile 操作是異步的,所以會移交給 Node.js,之后程序將會繼續(xù)執(zhí)行。
do while 操作持續(xù) 3ms。在這段時間內(nèi),readFile 操作完成并被推送到 IO 隊列。完成此操作后,事件循環(huán)將會開始檢查隊列。
盡管首先填充了檢查隊列,但只有在 IO 隊列為空之后才考慮使用它。所以在 setImmediate 之前,將 readFile 輸出到控制臺。
關閉隊列(Close queue)
此隊列存儲與關閉事件操作關聯(lián)的函數(shù)。
包括以下內(nèi)容:
- 流關閉事件,在關閉流時發(fā)出。它表示不再發(fā)出任何事件。
- http關閉事件,在服務器關閉時發(fā)出。
這些隊列被認為是優(yōu)先級最低的,因為此處的操作會在以后發(fā)生。
你肯sing不希望在處理 promise 函數(shù)之前在 close 事件中執(zhí)行回調(diào)函數(shù)。當服務器已經(jīng)關閉時,promise 函數(shù)會做些什么呢?
隊列順序
微任務隊列具有最高優(yōu)先級,其次是計時器隊列,I/O隊列,檢查隊列,最后是關閉隊列。
回調(diào)隊列的例子
讓我們通過一個更復雜的例子來說明隊列的類型和順序:
const fs = require("fs");// 假設此操作需要 2ms fs.writeFile('./new-file.json', '...', function() {console.log('writeFile') })// 假設這需要 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 將回調(diào)函數(shù)添加到 IO 隊列之前,fs.writeFile 在后臺花費 2 毫秒。
fs.readFile takes 10ms at the background before Node.js adds the callback function to the IO queue.
- 在 Node.js 將回調(diào)函數(shù)添加到 IO 隊列之前,fs.readFile 在后臺花費 10 毫秒。
- 在 Node.js 將回調(diào)函數(shù)添加到計時器隊列之前,setTimeout 在后臺花費 1ms。
- 現(xiàn)在,while 操作(同步)需要 3ms。在此期間,線程被阻止(請記住 JavaScript 是單線程的)。
- 同樣在這段時間內(nèi),setTimeout 和 fs.writeFile 操作完成,并將它們的回調(diào)函數(shù)分別添加到計時器和 IO 隊列中。
現(xiàn)在的隊列是:
// queues Timer = [function () {console.log("setTimeout");}, ]; IO = [function () {console.log("writeFile");}, ];setImmediate 將回調(diào)函數(shù)添加到 Check 隊列中:
js // 隊列 Timer... IO... Check = [function() {console.log("setImmediate")} ]在將 promise 操作添加到微任務隊列之前,需要花費 4ms 的時間在后臺進行解析。
最后一行是同步的,因此將會立即執(zhí)行:
# 返回 "last line"因為所有同步活動都已完成,所以事件循環(huán)開始檢查隊列。由于微任務隊列為空,因此它從計時器隊列開始:
// 隊列 Timer = [] // 現(xiàn)在是空的 IO... Check...# 返回 "last line" "setTimeout"當事件循環(huán)繼續(xù)執(zhí)行隊列中的回調(diào)函數(shù)時,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"最后,執(zhí)行所有回調(diào)函數(shù):
// 隊列Timer = []Microtask = []IO = [] // 現(xiàn)在又是空的Check = [];# results"last line""setTimeout""writeFile""setImmediate""promise""readFile"這里要注意的三點:
- 異步操作取決于添加到隊列之前的延遲時間。并不取決于它們在程序中的存放順序。
- 事件循環(huán)在每次迭代之繼續(xù)檢查其他任務之前,會連續(xù)檢查微任務隊列。
- 即使在后臺有另一個 IO 操作(readFile),事件循環(huán)也會執(zhí)行檢查隊列中的函數(shù)。這樣做的原因是此時 IO 隊列為空。請記住,在執(zhí)行 IO 隊列中的所有的函數(shù)之后,將會立即運行檢查隊列回調(diào)。
總結
JavaScript 是單線程的。每個異步函數(shù)都由依賴操作系統(tǒng)內(nèi)部函數(shù)工作的 Node.js 去處理。
Node.js 負責將回調(diào)函數(shù)(通過 JavaScript 附加到異步操作)添加到回調(diào)隊列中。事件循環(huán)會確定將要在每次迭代中接下來要執(zhí)行的回調(diào)函數(shù)。
了解隊列如何在 Node.js 中工作,使你對其有了更好的了解,因為隊列是環(huán)境的核心功能之一。 Node.js 最受歡迎的定義是 non-blocking(非阻塞),這意味著異步操作可以被正確的處理。都是因為有了事件循環(huán)和回調(diào)隊列才能使此功能生效。
原文:https://blog.logrocket.com/a-deep-dive-into-queues-in-node-js/
總結
以上是生活随笔為你收集整理的js 延迟几秒执行_深入研究 Node.js 的回调队列的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决摇一摇广告弹窗?努比亚Z50 Ult
- 下一篇: kafka原理_P8架构师带你参透Kaf