是時候使用 Node 8 了嗎?
已發表: 2022-03-11節點 8 出來了! 事實上,Node 8 現在已經出現了足夠長的時間,可以看到一些可靠的實際使用情況。 它帶有一個快速的新 V8 引擎和新特性,包括 async/await、HTTP/2 和 async 鉤子。 但它為您的項目做好準備了嗎? 讓我們來了解一下!
編者註:您可能已經知道 Node 10(代號為Dubnium )也已發布。 我們選擇關注節點 8 ( Carbon ) 有兩個原因:(1) 節點 10 剛剛進入其長期支持 (LTS) 階段,(2) 節點 8 標誌著比節點 10 更重要的迭代.
Node 8 LTS 中的性能
我們將首先了解這個非凡版本的性能改進和新功能。 一個主要的改進領域是 Node 的 JavaScript 引擎。
到底什麼是JavaScript 引擎?
JavaScript 引擎執行和優化代碼。 它可以是標準解釋器或將 JavaScript 編譯為字節碼的即時 (JIT) 編譯器。 Node.js 使用的 JS 引擎都是 JIT 編譯器,而不是解釋器。
V8 引擎
Node.js 從一開始就使用 Google 的Chrome V8 JavaScript 引擎,或者簡稱V8 。 一些 Node 版本用於與較新版本的 V8 同步。 但請注意不要將 V8 與 Node 8 混淆,因為我們在這裡比較 V8 版本。
這很容易被絆倒,因為在軟件上下文中,我們經常使用“v8”作為俚語,甚至是“版本 8”的官方縮寫,所以有些人可能會將“Node V8”或“Node.js V8”與“NodeJS 8”混為一談,”但我們在整篇文章中都避免了這一點,以幫助保持清晰: V8 永遠意味著引擎,而不是 Node.js 的版本。
V8 第 5 版
Node 6 使用 V8 第 5 版作為其 JavaScript 引擎。 (Node 8 的前幾個版本也使用 V8 版本 5,但它們使用的 V8 版本比 Node 6 更新。)
編譯器
V8 版本 5 及更早版本有兩個編譯器:
- Full-codegen是一種簡單快速的 JIT 編譯器,但生成的機器代碼很慢。
- Crankshaft是一種複雜的 JIT 編譯器,可以生成優化的機器代碼。
線程
在內心深處,V8 使用了不止一種類型的線程:
- 主線程獲取代碼,編譯它,然後執行它。
- 輔助線程在主線程優化代碼時執行代碼。
- 探查器線程通知運行時有關性能不佳的方法。 然後曲軸優化這些方法。
- 其他線程管理垃圾收集。
編譯過程
首先,Full-codegen 編譯器執行 JavaScript 代碼。 在執行代碼時,分析器線程收集數據以確定引擎將優化哪些方法。 在另一個線程上,Crankshaft 優化了這些方法。
問題
上述方法有兩個主要問題。 首先,它在架構上很複雜。 其次,編譯後的機器代碼會消耗更多的內存。 消耗的內存量與代碼執行的次數無關。 即使只運行一次的代碼也會佔用大量內存。
V8 版本 6
第一個使用 V8 第 6 版引擎的 Node 版本是 Node 8.3。
在第 6 版中,V8 團隊構建了 Ignition 和 TurboFan 來緩解這些問題。 Ignition 和 TurboFan 分別替換了 Full-codegen 和 CrankShaft。
新架構更直接,消耗更少的內存。
Ignition 將 JavaScript 代碼編譯為字節碼而不是機器碼,從而節省大量內存。 之後,優化編譯器 TurboFan 從這個字節碼生成優化的機器碼。
具體的性能改進
讓我們看看 Node 8.3+ 中的性能相對於舊 Node 版本發生了哪些變化。
創建對象
在 Node 8.3+ 中創建對象的速度大約是 Node 6 中的五倍。
函數大小
V8 引擎根據幾個因素決定是否應優化功能。 一個因素是函數大小。 小函數得到優化,而長函數則沒有。
函數大小如何計算?
舊 V8 引擎中的曲軸使用“字符數”來確定函數大小。 函數中的空格和註釋會降低優化的機會。 我知道這可能會讓您大吃一驚,但當時,評論可能會使速度降低約 10%。
在 Node 8.3+ 中,空格和註釋等無關字符不會損害函數性能。 為什麼不?
因為新的 TurboFan 不計算字符來確定函數大小。 相反,它計算抽象語法樹 (AST) 節點,因此它只考慮實際的功能指令。 使用 Node 8.3+,您可以根據需要添加註釋和空格。
Array
化參數
JavaScript 中的常規函數帶有一個隱式的類似Array
的argument
對象。
類Array
是什麼意思?
arguments
對象的行為有點像數組。 它具有length
屬性,但缺少Array
的內置方法,如forEach
和map
。
以下是arguments
對象的工作方式:
function foo() { console.log(arguments[0]); // Expected output: a console.log(arguments[1]); // Expected output: b console.log(arguments[2]); // Expected output: c } foo("a", "b", "c");
那麼我們如何將arguments
對象轉換為數組呢? 通過使用簡潔的Array.prototype.slice.call(arguments)
。
function test() { const r = Array.prototype.slice.call(arguments); console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output: [2, 4, 6]
Array.prototype.slice.call(arguments)
會影響所有 Node 版本的性能。 因此,通過for
循環複製鍵執行得更好:
function test() { const r = []; for (index in arguments) { r.push(arguments[index]); } console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]
for
循環有點麻煩,不是嗎? 我們可以使用擴展運算符,但在 Node 8.2 及以下版本中它很慢:
function test() { const r = [...arguments]; console.log(r.map(num => num * 2)); } test(1, 2, 3); // Expected output [2, 4, 6]
Node 8.3+ 中的情況發生了變化。 現在傳播的執行速度要快得多,甚至比 for 循環還要快。
部分應用(Currying)和綁定
柯里化是將一個接受多個參數的函數分解為一系列函數,其中每個新函數只接受一個參數。
假設我們有一個簡單的add
函數。 這個函數的柯里化版本有一個參數num1
。 它返回一個函數,該函數接受另一個參數num2
並返回num1
和num2
之和:
function add(num1, num2) { return num1 + num2; } add(4, 6); // returns 10 function curriedAdd(num1) { return function(num2) { return num1 + num2; }; } const add5 = curriedAdd(5); add5(3); // returns 8
bind
方法返回一個語法更簡潔的柯里化函數。
function add(num1, num2) { return num1 + num2; } const add5 = add.bind(null, 5); add5(3); // returns 8
所以bind
是不可思議的,但是在舊的 Node 版本中它很慢。 在 Node 8.3+ 中, bind
更快,您可以使用它而不必擔心任何性能損失。
實驗
已經進行了幾個實驗來比較節點 6 和節點 8 的性能。 請注意,這些是在 Node 8.0 上進行的,因此它們不包括上面提到的 Node 8.3+ 特有的改進,這要歸功於其 V8 版本 6 升級。
節點 8 中的服務器渲染時間比節點 6 少 25%。在大型項目中,服務器實例的數量可以從 100 個減少到 75 個。這令人驚訝。 在 Node 8 中測試一組 500 個測試的速度提高了 10%。 Webpack 構建速度提高了 7%。 總的來說,結果顯示 Node 8 的性能顯著提升。
節點 8 功能
速度並不是 Node 8 中唯一的改進。它還帶來了幾個方便的新特性——也許最重要的是, async/await 。
節點 8 中的異步/等待
回調和承諾通常用於處理 JavaScript 中的異步代碼。 回調因產生不可維護的代碼而臭名昭著。 它們在 JavaScript 社區中引起了混亂(特別是回調地獄)。 Promise 將我們從回調地獄中拯救了很長時間,但它們仍然缺乏同步代碼的簡潔性。 Async/await 是一種現代方法,允許您編寫看起來像同步代碼的異步代碼。
雖然 async/await 可以在以前的 Node 版本中使用,但它需要外部庫和工具——例如,通過 Babel 進行額外的預處理。 現在它可以在本地使用,開箱即用。
我將討論 async/await 優於傳統承諾的一些情況。
條件句
假設您正在獲取數據,您將根據有效負載確定是否需要新的 API 調用。 看看下面的代碼,看看這是如何通過“傳統承諾”方法完成的。
const request = () => { return getData().then(data => { if (!data.car) { return fetchForCar(data.id).then(carData => { console.log(carData); return carData; }); } else { console.log(data); return data; } }); };
正如你所看到的,上面的代碼看起來已經很亂了,只是來自一個額外的條件。 Async/await 涉及較少的嵌套:
const request = async () => { const data = await getData(); if (!data.car) { const carData = await fetchForCar(data); console.log(carData); return carData; } else { console.log(data); return data; } };
錯誤處理
Async/await 授予您處理 try/catch 中的同步和異步錯誤的權限。 假設您要解析來自異步 API 調用的 JSON。 單個 try/catch 可以處理解析錯誤和 API 錯誤。
const request = async () => { try { console.log(await getData()); } catch (err) { console.log(err); } };
中間值
如果一個 Promise 需要一個應該從另一個 Promise 解決的參數怎麼辦? 這意味著需要串行執行異步調用。
使用傳統的 Promise,您最終可能會得到如下代碼:
const request = () => { return fetchUserData() .then(userData => { return fetchCompanyData(userData); }) .then(companyData => { return fetchRetiringPlan(userData, companyData); }) .then(retiringPlan => { const retiringPlan = retiringPlan; }); };
Async/await 在這種情況下大放異彩,在這種情況下需要鍊式異步調用:
const request = async () => { const userData = await fetchUserData(); const companyData = await fetchCompanyData(userData); const retiringPlan = await fetchRetiringPlan(userData, companyData); };
並行異步
如果要並行調用多個異步函數怎麼辦? 在下面的代碼中,我們將等待fetchHouseData
解析,然後調用fetchCarData
。 儘管它們中的每一個都相互獨立,但它們是按順序處理的。 您將等待兩秒鐘讓兩個 API 解析。 這是不好的。
function fetchHouseData() { return new Promise(resolve => setTimeout(() => resolve("Mansion"), 1000)); } function fetchCarData() { return new Promise(resolve => setTimeout(() => resolve("Ferrari"), 1000)); } async function action() { const house = await fetchHouseData(); // Wait one second const car = await fetchCarData(); // ...then wait another second. console.log(house, car, " in series"); } action();
更好的方法是並行處理異步調用。 檢查下面的代碼以了解這是如何在 async/await 中實現的。

async function parallel() { houseDataPromise = fetchHouseData(); carDataPromise = fetchCarData(); const house = await houseDataPromise; // Wait one second for both const car = await carDataPromise; console.log(house, car, " in parallel"); } parallel();
並行處理這些調用讓您只需等待一秒鐘即可完成這兩個調用。
新的核心庫函數
Node 8 還帶來了一些新的核心功能。
複製文件
在 Node 8 之前,為了複製文件,我們曾經創建兩個流並將數據從一個流傳輸到另一個。 下面的代碼顯示了讀取流如何將數據傳輸到寫入流。 正如您所看到的,對於復製文件這樣簡單的操作,代碼是雜亂無章的。
const fs = require('fs'); const rd = fs.createReadStream('sourceFile.txt'); rd.on('error', err => { console.log(err); }); const wr = fs.createWriteStream('target.txt'); wr.on('error', err => { console.log(err); }); wr.on('close', function(ex) { console.log('File Copied'); }); rd.pipe(wr);
在 Node 8 中, fs.copyFile
和fs.copyFileSync
是複製文件的新方法,更輕鬆。
const fs = require("fs"); fs.copyFile("firstFile.txt", "secondFile.txt", err => { if (err) { console.log(err); } else { console.log("File copied"); } });
承諾和回調
util.promisify
將常規函數轉換為異步函數。 請注意,輸入的函數應遵循常見的 Node.js 回調樣式。 它應該將回調作為最後一個參數,即(error, payload) => { ... }
。
const { promisify } = require('util'); const fs = require('fs'); const readFilePromisified = promisify(fs.readFile); const file_path = process.argv[2]; readFilePromisified(file_path) .then((text) => console.log(text)) .catch((err) => console.log(err));
如您所見, util.promisify
已將fs.readFile
轉換為異步函數。
另一方面,Node.js 帶有util.callbackify
。 util.callbackify
與util.promisify
相反:它將異步函數轉換為 Node.js 回調樣式函數。
可讀可寫的destroy
函數
Node 8 中的destroy
函數是一種用於銷毀/關閉/中止可讀或可寫流的記錄方法:
const fs = require('fs'); const file = fs.createWriteStream('./big.txt'); file.on('error', errors => { console.log(errors); }); file.write(`New text.\n`); file.destroy(['First Error', 'Second Error']);
上面的代碼會創建一個名為big.txt
的新文件(如果它不存在),其中包含文本New text.
.
Node 8 中的Readable.destroy
和Writeable.destroy
函數會發出一個close
事件和一個可選的error
事件—— destroy
並不一定意味著任何錯誤。
擴展運算符
擴展運算符(又名...
)在 Node 6 中工作,但僅適用於數組和其他可迭代對象:
const arr1 = [1,2,3,4,5,6] const arr2 = [...arr1, 9] console.log(arr2) // expected output: [1,2,3,4,5,6,9]
在 Node 8 中,對像也可以使用擴展運算符:
const userCarData = { type: 'ferrari', color: 'red' }; const userSettingsData = { lastLoggedIn: '12/03/2019', featuresPlan: 'premium' }; const userData = { ...userCarData, name: 'Youssef', ...userSettingsData }; console.log(userData); /* Expected output: { type: 'ferrari', color: 'red', name: 'Youssef', lastLoggedIn: '12/03/2019', featuresPlan: 'premium' } */
Node 8 LTS 中的實驗功能
實驗性功能不穩定,可能會被棄用,並且可能會隨著時間的推移而更新。 在它們變得穩定之前,不要在生產中使用任何這些功能。
異步鉤子
異步掛鉤通過 API 跟踪在 Node 內部創建的異步資源的生命週期。
在進一步使用異步鉤子之前,請確保您了解事件循環。 這個視頻可能會有所幫助。 異步鉤子對於調試異步函數很有用。 它們有幾個應用程序; 其中之一是異步函數的錯誤堆棧跟踪。
看看下面的代碼。 請注意, console.log
是一個異步函數。 因此它不能在異步鉤子中使用。 而是使用fs.writeSync
。
const asyncHooks = require('async_hooks'); const fs = require('fs'); const init = (asyncId, type, triggerId) => fs.writeSync(1, `${type} \n`); const asyncHook = asyncHooks.createHook({ init }); asyncHook.enable();
觀看此視頻以了解有關異步掛鉤的更多信息。 具體就 Node.js 指南而言,本文通過一個說明性的應用程序幫助揭開異步掛鉤的神秘面紗。
Node 8 中的 ES6 模塊
Node 8 現在支持 ES6 模塊,使您能夠使用以下語法:
import { UtilityService } from './utility_service';
要在 Node 8 中使用 ES6 模塊,您需要執行以下操作。
- 將
--experimental-modules
標誌添加到命令行 - 將文件擴展名從
.js
重命名為.mjs
HTTP/2
HTTP/2 是對不經常更新的 HTTP 協議的最新更新,Node 8.4+ 在實驗模式下原生支持它。 它比其前身 HTTP/1.1 更快、更安全、更高效。 谷歌建議你使用它。 但它還有什麼作用?
多路復用
在 HTTP/1.1 中,服務器一次只能為每個連接發送一個響應。 在 HTTP/2 中,服務器可以並行發送多個響應。
服務器推送
服務器可以為單個客戶端請求推送多個響應。 為什麼這是有益的? 以 Web 應用程序為例。 按照慣例,
- 客戶端請求一個 HTML 文檔。
- 客戶端從 HTML 文檔中發現所需的資源。
- 客戶端為每個所需資源發送一個 HTTP 請求。 例如,客戶端為文檔中提到的每個 JS 和 CSS 資源發送一個 HTTP 請求。
服務器推送功能利用了服務器已經知道所有這些資源的事實。 服務器將這些資源推送到客戶端。 因此,對於 Web 應用程序示例,在客戶端請求初始文檔後,服務器會推送所有資源。 這減少了延遲。
優先級
客戶端可以設置優先級方案來確定每個所需響應的重要性。 然後,服務器可以使用此方案來優先分配內存、CPU、帶寬和其他資源。
改掉舊的壞習慣
由於 HTTP/1.1 不允許多路復用,因此使用了一些優化和變通方法來掩蓋緩慢的速度和文件加載。 不幸的是,這些技術會導致 RAM 消耗增加和渲染延遲:
- 域分片:使用多個子域,以便連接分散並並行處理。
- 結合 CSS 和 JavaScript 文件以減少請求的數量。
- Sprite maps:組合圖像文件以減少 HTTP 請求。
- 內聯:將 CSS 和 JavaScript 直接放在 HTML 中,以減少連接數。
現在使用 HTTP/2,您可以忘記這些技術並專注於您的代碼。
但是如何使用 HTTP/2?
大多數瀏覽器僅通過安全的 SSL 連接支持 HTTP/2。 本文可以幫助您配置自簽名證書。 在名為ssl
的目錄中添加生成的.crt
文件和.key
文件。 然後,將以下代碼添加到名為server.js
的文件中。
請記住在命令行中使用--expose-http2
標誌來啟用此功能。 即我們示例的運行命令是node server.js --expose-http2
。
const http2 = require('http2'); const path = require('path'); const fs = require('fs'); const PORT = 3000; const secureServerOptions = { cert: fs.readFileSync(path.join(__dirname, './ssl/server.crt')), key: fs.readFileSync(path.join(__dirname, './ssl/server.key')) }; const server = http2.createSecureServer(secureServerOptions, (req, res) => { res.statusCode = 200; res.end('Hello from Toptal'); }); server.listen( PORT, err => err ? console.error(err) : console.log(`Server listening to port ${PORT}`) );
當然,Node 8、Node 9、Node 10 等仍然支持舊的 HTTP 1.1——關於標準 HTTP 事務的官方 Node.js 文檔不會過時很長時間。 但是如果你想使用 HTTP/2,你可以更深入地閱讀這個 Node.js 指南。
那麼,我到底應該使用 Node.js 8 嗎?
Node 8 帶來了性能改進以及 async/await、HTTP/2 等新功能。 端到端實驗表明,Node 8 比 Node 6 快約 25%。這可以節省大量成本。 所以對於新建項目,絕對! 但是對於現有的項目,你應該更新 Node 嗎?
這取決於您是否需要更改大部分現有代碼。 如果您來自 Node 6,本文檔列出了所有 Node 8 重大更改。請記住,通過使用最新的 Node 8 版本重新安裝項目的所有npm
包來避免常見問題。 此外,始終在開發機器上使用與生產服務器上相同的 Node.js 版本。 祝你好運!
- 為什麼我會使用 Node.js? 個案教程
- 調試 Node.js 應用程序中的內存洩漏
- 在 Node.js 中創建安全 REST API
- Cabin Fever 編碼:Node.js 後端教程