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( ).slice(1).join( ); const extraStack = stack.get(triggerId || currentUid); stack.set(id, localStack + + 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(); 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. (/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 ... 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++ 模塊的版本不兼容問題,但這也給用戶帶來了使用的不便。見:"It should match the enclosed Node.js runtime version of the compiler." · Issue #27 · pmq20/node-compiler如果能做到上述最好的情況,那麼這個問題也就完美解決了。
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 我們可以用 await/async 書寫程序,但並未提到異常處理,其實 await/async 的異常處理多藉助 try/catch 配合使用。而在以前的 Node.js 版本中,try/catch 是個昂貴的操作,性能並不高。這主要是由於 v8 內老的 Crankshaft 不易優化這些 ES5 的新語法。但隨著 TF+I 新架構的引入,try/catch 的寫法也可以得到優化,作為用戶就可以高枕無憂的使用 await/async + try/catch 了。
※如何用一份產品規劃書代替BRD、MRD 競品分析?
※你會如何設計 1000 層大樓的電梯按鈕
※React 狀態管理庫:Mobx
※越過代理商,直奔社交網紅…廣告主正掀起「去中介化」運動!
※三隻松鼠開在南京的最大一家線下投食店 效果還好嗎?
TAG:推酷 |
※大規模集群下的Hadoop NameNode
※log4js-node配置
※Node.js之express框架
※Node.js用戶想學Rust
※Node.js進階:cluster模塊深入剖析
※2分鐘看懂 Node.js 精髓
※blogfoster-scripts:一款簡化 Node.js 項目初始化的工具
※Node.js 主題周
※Electron 4.0 穩定版發布,集成 Node 10 和 Chromium 69
※deno 如何償還 node.js 的十大技術債
※Webpack 4.0.0 beta.0 發布,不再支持 Node.js 4
※至薄簡約の小鋼炮,Fractal Design Node 202 Slim 裝機作業
※node+pm2+express+mysql+sequelize來搭建網站和寫介面
※DOM探索之-DOM的nodeType、nodeName、nodeValue
※基於 node.js 的自動路由組件-HttpPostman
※Google發布Knative,IBM發布雲原生Node.js應用的資源
※掌握 Node.js的8 個技巧
※Node.js應用Linux部署實戰
※前端視界:Chrome71Beta、Node.js11、Reactv16.6.0、RN0.57.4、Angular7.0.1……
※拒絕 Python、C 和 Go,我只用 Node.js!