端午節後福利:Node.js 8
端午節結束了。雖然接下來的四個月都沒有節假日,但筆者一點都不煩惱。因為 Node.js 8 在端午後第一個工作日就正式釋出,這足以讓我與 Node.js 的激情燃燒一個夏天!本文挑選了筆者認為 Node.js 8 最令人興奮的四大新功能,與大家分享。
async/await 與 util.promisify
Node.js 一直以來的關鍵設計就是把使用者關在一個“非同步程式設計的監獄”裡,以換取非阻塞 I/O 的高效能,讓使用者輕易開發出高度可擴充套件的網路伺服器。這從 Node.js 的 API 設計上就可見一斑,很多API——如 fs.open(path, flags[, mode], callback)——要求使用者必須把該操作執行成功後的邏輯放在最後引數裡,作為函式傳遞進去;而 fs.open 本身是立即返回的,使用者不能把依賴於 fs.open 結果的邏輯與 fs.open 本身線性地串聯起來。
在這座“非同步程式設計的監獄”裡,不掌握非同步程式設計就寸步難行。而我們習慣性地使用線性思維去思考業務問題,卻在實現的時候,被迫把業務邏輯被切成很多小片段去書寫,就是一件很痛苦的事情了。為了減輕非同步程式設計的痛苦,幾年間我們見證了數個解決方案的出現:從深度巢狀的回撥金字塔,到帶有長長的 then() 鏈條的 Promise 設計模式,再到 Generator 函式,到如今 Node.js 8 的 async/await 操作符。筆者認為,所有這些解決方案中,async/await 操作符是最接近指令式程式設計風格的,使用起來最為自然的。
例如我們想先建立一個檔案,再讀取、輸出它的大小,只需三行程式碼:
await writeFile('a_new_file.txt', 'hello');
let result = await stat('a_new_file.txt');
console.log(result.size);
這簡直是最簡單的非同步程式設計了!我們用自然、流暢的程式碼表達了線性業務邏輯,同時還得到了 Node.js 非阻塞 I/O 帶來的高效能,簡直是兼得了魚和熊掌。
但彆著急,這段程式碼不是立即就可以執行的,細心的讀者肯定會問:例子中的 writeFile 和 stat 分別是什麼?其實它們就是標準庫的 fs.writeFile 和 fs.stat,但又不完全是。這是因為 async 和 await 本質上是對 Promise 設計模式的封裝,一般情況下 await 的引數應是一個返回 Promise 物件的函式。而 fs.writeFile 和 fs.stat 這些標準庫 API 沒有返回值(返回 undefined),需要一個方法把他們包裝成返回 Promise 物件的函式。
但總不能一個一個包裝去吧,這樣工作量堪比重寫標準庫了。幸好,我們觀察到所有這些標準庫 API 基本都滿足一個共同特徵:它們都是用最後一個引數來接受一個類似“ (err, value) => ... ”的回撥函式。於是我們就可以用一個 API 把幾乎所有標準庫 API 都轉換為返回 Promise 物件的函式。這就是 util.promisify。利用 util.promisify,我們可以新增以下程式碼:
const util = require('util');
const fs = require('fs');
const writeFile = util.promisify(fs.writeFile);
const stat = util.promisify(fs.stat);
若沒有 util.promisify,async/await 是很難用的,因為它們需要配合 Promise 一起使用,而之前很多庫函式又不返回 Promise。筆者認為 async/await 運算子和 util.promisify 的絕配,是 Node.js 8 最大的亮點。
以上示例的完整程式碼如下:
const util = require('util');
const fs = require('fs');
const writeFile = util.promisify(fs.writeFile);
const stat = util.promisify(fs.stat);
(async function () {
await writeFile('a_new_file.txt', 'hello');
let result = await stat('a_new_file.txt');
console.log(result.size);
})();
Async Hooks
除錯過 Node.js 的小夥伴都知道,Node.js 一個很大的弱點就是——出錯時呼叫棧不完整。這也是“非同步程式設計的監獄”的設計帶來的另一個缺點,因為在非同步程式設計下,我們的程式碼被切成了無數個小片段,報錯時只能得到一個小片段的呼叫棧,而全域性的來龍去脈卻看不到,使用者只能推測是何處程式碼觸發了何種事件導致執行了小片段,再不斷往前推演。
舉一個簡單的例子:
function IWantFullCallbacks() {
setTimeout(function() {
const localStack = new Error();
console.log(localStack.stack);
}, 1000);
}
IWantFullCallbacks();
在這個例子中,我們模擬了 setTimeout 內出錯時列印呼叫棧的情景。將它存為 1.js 並執行,我們期望,如果呼叫棧能包含外層的 IWantFullCallbacks(),並列印其行號 8,定是極好的,因為那樣對我們排查錯誤很有幫助。但現實中卻並非如此,呼叫棧只有四行,行號頂多列印到了第 3 行的報錯本身,我們根本看不出來是第 8 行觸發了這個錯誤。因為第 8 行作為非同步呼叫成功地結束了,它才不關心“後事如何”。
Error
at Timeout._onTimeout (/Users/pmq20/1.js:3:24)
at ontimeout (timers.js:488:11)
at tryOnTimeout (timers.js:323:5)
at Timer.listOnTimeout (timers.js:283:5)
而 Node.js 8 中新增的 Async Hooks 功能就可以解決這個問題。Node.js 8 中新增了四種 Async Hooks 回撥,它們可以跟蹤 Node.js 的所有非同步資源的生命週期。這裡所謂的資源是指 Node.js 底層 libuv 中的各類短期請求和長期控制程式碼,如本例中的定時器,就是這樣一個非同步資源。這四種回撥分別涵蓋了這些非同步資源的建立、回撥前、回撥後、銷燬這四個生命階段。
通過自定義這四種回撥函式,我們就可以跨呼叫棧來做事件追蹤,我們可以先做一個 Map 容器放在回撥函式的閉包裡,用來作為非同步資源 ID 到除錯資訊的對映,並在非同步資源的建立時進行除錯資訊的累積。閉包裡再宣告一個 currentUid 表示目前正在執行的非同步資源 ID,於回撥前、回撥後兩個生命階段的時機進行記錄。這樣下來,第 8 行執行 IWantFullCallbacks() 的時候建立的非同步資源的 ID,與後期定時器到期自行回撥的非同步資源的 ID,是同一個 ID,因而可以起到跨呼叫棧累積除錯資訊的作用。我們通過 Node.js 8 的 async_hooks 的 createHook API 來建立回撥,並通過 enable() 方法註冊並執行,程式碼如下:
const stack = new Map();
stack.set(-1, '');
let currentUid = -1;
function init(id, type, triggerId, resource) {
const localStack = (new Error()).stack.split('\n').slice(1).join('\n');
const extraStack = stack.get(triggerId || currentUid);
stack.set(id, localStack + '\n' + extraStack);
}
function before(uid) {
currentUid = uid;
}
function after(uid) {
currentUid = -1;
}
function destroy(uid) {
stack.delete(uid);
}
const async_hooks = require('async_hooks');
const hook = async_hooks.createHook({init, before, after, destroy});
hook.enable();
最後我們修改定時器的回撥內容,讓它輸出 Map 中累積的除錯資訊:
function IWantFullCallbacks() {
setTimeout(function() {
const localStack = new Error();
console.log(localStack.stack);
console.log('---');
console.log(stack.get(currentUid));
}, 1000);
}
這次的效果如下:
Error
at Timeout._onTimeout (/Users/pmq20/2.js:26:24)
at ontimeout (timers.js:488:11)
at tryOnTimeout (timers.js:323:5)
at Timer.listOnTimeout (timers.js:283:5)
---
at init (/Users/pmq20/2.js:6:23)
at runInitCallback (async_hooks.js:459:5)
at emitInitS (async_hooks.js:327:7)
at new Timeout (timers.js:592:5)
at createSingleTimeout (timers.js:472:15)
at setTimeout (timers.js:456:10)
at IWantFullCallbacks (/Users/pmq20/2.js:25:3)
at Object.<anonymous> (/Users/pmq20/2.js:33:1)
at Module._compile (module.js:569:30)
at Object.Module._extensions..js (module.js:580:10)
可見,我們以同一個非同步資源的 ID 為線索,把兩次的呼叫棧都完整保留了。
但這只是 Node.js 8 的 Async Hooks 的用途之一,有了這個功能,我們甚至可以來測量一些事件各個階段所花費的時間。只要我們有非同步資源 ID 這枚鑰匙,配合回撥函式,就可以在事件迴圈的多個週期那看似毫無頭緒的執行過程中,篩選出有用的資訊。
Node.js API (N-API)
經歷過 Node.js 大版本升級的同學肯定會發現,每次升級後我們都得重新編譯像 node-sass 這種用 C++ 寫的擴充套件模組,否則會遇到下面這樣的報錯,
Error: The module 'node-sass'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 51. This version of Node.js requires
NODE_MODULE_VERSION 55. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
NODE_MODULE_VERSION 是每一個 Node.js 版本內人為設定的數值,意思為 ABI 的版本號。一旦這個號碼與已經編譯好的二進位制模組的號碼不符,便判斷為 ABI 不相容,需要使用者重新編譯。
這其實是一個工程難題,亦即 Node.js 上游的程式碼變化如何最小地降低對 C++ 模組的影響,從而維持一個良好的向下相容的模組生態系統。最壞的情況下,每次釋出 Node.js 新版本,因為 API 的變化,C++ 模組的作者都要修改它們的原始碼,而那些不再有人維護或作者失聯的老模組就會無法繼續使用,在作者修改程式碼之前社群就失去了這些模組的可用性。其次壞的情況是,每次釋出 Node.js 新版本,雖然 API 保持相容使得 C++ 模組的作者不需要修改他們的程式碼,但 ABI 的變化導致必須這些模組必須重新編譯。而最好的情況就是,Node.js 新版本釋出後,所有已編譯的 C++ 模組可以繼續正常工作,完全不需要任何人工干預。
Node.js Compiler 也面臨同樣的問題,之前 nodec 強制使用者編譯環境中的 Node.js 版本與編譯器的內建 Node.js 版本一致,就是為了消除編譯時與執行時 C++ 模組的版本不相容問題,但這也給使用者帶來了使用的不便。見: https://github.com/pmq20/node-compiler/issues/27 如果能做到上述最好的情況,那麼這個問題也就完美解決了。
Node.js 8 的 Node.js API (N-API) 就是為了解決這個問題,做到上述最好的情況,為 Node.js 模組生態系統的長期發展鋪平道路。N-API 追求以下目標:
- 有穩定的 ABI
- 抽象消除 Node.js 版本之間的介面差異
- 抽象消除 V8 版本之間的介面差異
- 抽象消除 V8 與其他 JS 引擎(如 ChakraCore)之間的介面差異
筆者觀察到,N-API 採取以下手段達到上述目標:
- 採用 C 語言標頭檔案而不是 C++,消除 Name Mangling 以便最小化一個穩定的 ABI 介面
- 不使用 V8 的任何資料型別,所有 JavaScript 資料型別變成了不透明的 napi_value
- 重新設計了異常管理 API,所有 N-API 都返回 napi_status,通過統一的手段處理異常
- 重新了設計物件的生命週期 API,通過 napi_open_handle_scope 等 API 替代了 v8 的 Scope 設計
N-API 目前在 Node.js 8 仍是實驗階段的功能,需要配合命令列引數 --napi-modules 使用。
TurboFan 與 Ignition (TF+I)
Node.js 8 得益於將 v8 升級到了 5.8,引入了 TurboFan 與 Ignition 的支援。關於 Ignition 的詳細介紹,請見 https://www.atatech.org/articles/78497
前面已經提到,如今藉助 Node.js 8 我們可以用 await/async 書寫程式,但並未提到異常處理,其實 await/async 的異常處理多借助 try/catch 配合使用。而在以前的 Node.js 版本中,try/catch 是個昂貴的操作,效能並不高。這主要是由於 v8 內老的 Crankshaft 不易優化這些 ES5 的新語法。但隨著 TF+I 新架構的引入,try/catch 的寫法也可以得到優化,作為使用者就可以高枕無憂的使用 await/async + try/catch 了。
目前 Node.js 8 內的 v8 版本僅更新到了 5.8,TF+I 需要配合命令列引數 --turbo --ignition 使用。一旦升級到 v8 5.9,TF+I 將會預設開啟。
相關文章
- Node.js背後的V8引擎優化技術Node.js優化
- 8、Node.js Buffer(緩衝區)Node.js
- 端午節智慧手錶大推薦
- Node.js + TypeScript 寫後端工具Node.jsTypeScript後端
- 小白福利,零成本,配置小程式前後端後端
- 《推理學院》熱鬧端午節!精彩活動搶先看
- Node.js 8有哪些重要功能和修復?Node.js
- 15個最好用的Node.JS後端框架Node.js後端框架
- Node.js 新計劃:使用 V8 snapshot 將啟動速度提升 8 倍Node.js
- 後端知識點總結——NODE.JS(高階)後端Node.js
- 後端知識點總結——NODE.JS基礎後端Node.js
- 【新年福利】2019年值得一用的8款協作工具
- Golang福利爬蟲Golang爬蟲
- jsp福利喲JS
- 初步瞭解Express(基於node.js的後端框架)ExpressNode.js後端框架
- 變態手遊哪個平臺福利最好 福利遊戲app排行遊戲APP
- 【Java8新特性】冰河帶你看盡Java8新特性,你想要的都在這兒了!!(文字有福利)Java
- Java 8 後的新功能梳理Java
- java8之後的介面Java
- 超大福利 | 這款免費 Java 線上診斷利器,不用真的會後悔!Java
- 安裝好node.js之後載入模組,npm install colors之後報錯Node.jsNPM
- 去哪兒:2015年端午節出行資料包告
- bt手遊哪個平臺福利最好 福利最多的手遊平臺
- 基於node.js和oss的後端簽名直傳Node.js後端
- 使用Node.js和Koa框架實現前後端互動Node.js框架後端
- 基於Vue和Node.js的電商後臺管理系統VueNode.js
- 基於 Node.js 前後端分離的一點思考Node.js後端
- 圖解基於 Node.js 實現前後端分離圖解Node.js後端
- JavaScript 懶癌患者福利JavaScript
- PyCharm的學生福利PyCharm
- React + Node.JS 巧妙實現後臺管理系統の各種小技巧(前後端)ReactNode.js後端
- 為 Node.js 開發者準備的 8 本免費線上電子書Node.js
- Node.js實現前後端互動——使用者登陸Node.js後端
- 我為什麼向後端工程師推薦Node.js後端工程師Node.js
- 昆明邦芒福利管理外包 一站式福利外包解決方案
- 【正視CSS03】block與position,出門在外的朋友端午節快樂CSSBloC
- 遊戲福利,用微信就能搜?遊戲
- RestCloud·618福利預告RESTCloud