如何快速入門VUE3.0:進入學習
儘管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 執行,然而每一個階段基於它自己的方式執行,總體來講,當事件循環進入到一個階段裡,它將執行當前階段的任何操作,開始執行當前階段隊列中的回調直到隊列完全消耗完或執行到隊列的最大數據。當佇列消耗完或達到最大數量,事件循環就會移動到下一個階段。
在事件循環的每個過程中,Node .js檢查它是否正在等待非同步的I/O和計時器,如果沒有則完全關閉
一個計時器指定一個回調會被執行的臨界點,而不是人們想讓它執行的時間,計時器會在指定的過去時間之後盡可能早的執行,然而,作業系統調度或其他回調會讓它延遲執行。
從技術角度上講,poll 階段決定了回呼何時執行
例如,你設定了一個計時器,100 ms之後執行,然而你的腳本非同步讀取了一個檔案花了95ms
const fs = require('fs') ; function someAsyncOperation(callback) { // Assume this takes 95ms to complete fs.readFile('/path/to/file', 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 階段同樣也有一個最大值停止輪訓更多事件
此階段為某些系統操作(例如TCP 錯誤類型)執行回呼。 例如,如果TCP 套接字在嘗試連線時收到ECONNREFUSED,則某些*nix 系統希望等待報告錯誤。 這將在掛起的回調階段排隊執行。
poll 階段有兩個主要的功能
當事件循環進入到了poll階段並且沒有計時器,發生以下兩種事情
一旦poll 佇列是空的,事件循環會偵測計時器是否到時間,如果有,事件循環會到達timers 階段執行計時器回呼
此階段允許人們在poll 階段完成後立即執行回調。 如果輪詢階段變得空閒且腳本已使用setImmediate() 排隊,則事件循環可能會繼續到check 階段而不是等待。
setImmediate() 實際上是一個特殊的計時器,它在事件循環的單獨階段運行。 它使用一個libuv API 來安排在poll 階段完成後執行的回呼。
通常,隨著程式碼的執行,事件循環最終會到達poll 階段,它將等待傳入的連線、請求等。但是,如果使用setImmediate() 安排了回調並且poll 階段變得空閒,它將結束並繼續check 階段,而不是等待poll 事件。
如果一個socket 或操作突然被關閉(eg socket.destroy()),close 事件會被送到這個階段,否則會透過process.nextTick()發送
setImmediate() 和setTimeout( ) 是相似的,但是不同的行為取決於在什麼時候被調用
每個回調執行的順序依賴他們被調用的上下本環境,如果在同一個模組同時調用,那麼時間會受到進程性能的限制(這也會被運行在這台機器的其他應用所影響)
例如,如果我們不在I/O裡邊運行下面的腳本,儘管它受進程效能的影響,但不能夠確定這兩個計時器的執行順序:
// timeout_vs_immediate.js setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
$ node timeout_vs_immediate.js timeout immediate $ node timeout_vs_immediate.js immediate timeout
然而,如果你移動到I/O 迴圈中,immediate 回呼總是會先執行
// 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 immediate timeout $ node timeout_vs_immediate.js immediate timeout
setImmediate 相對於setTimeout 的優點是setImmediate 如果在I/O 中總是會優先於任何計時器先執行,與存在多少計時器無關。
儘管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 !== 'string') return process.nextTick( callback, new TypeError('argument should be string') ); }
此片段會進行參數檢查,如果不正確,它會將錯誤傳遞給回呼。 API 最近更新,允許將參數傳遞給process.nextTick() 允許它接受在回調之後傳遞的任何參數作為回調的參數傳播,因此您不必嵌套函數。
我們正在做的是將錯誤傳回給用戶,但前提是我們允許用戶的其餘程式碼執行。 透過使用process.nextTick(),我們保證apiCall() 總是在使用者程式碼的其餘部分之後和允許事件循環繼續之前運行它的回調。 為了實現這一點,允許JS 調用堆疊展開,然後立即執行提供的回調,這允許人們對process.nextTick() 進行遞歸調用,而不會達到RangeError:從v8 開始超出最大調用堆疊大小。