Async Hooks 是 Node8 新出來的特性,提供了一些 API 用于跟蹤 NodeJs 中的異步資源的生命周期,屬于 NodeJs 內置模塊,可以直接引用。
1
|
const async_hooks = require( 'async_hooks' ); |
這是一個很少使用的模塊,為什么會有這個模塊呢?
我們都知道,JavaScript在設計之初就是一門單線程語言,這和他的設計初衷有關,最初的JavaScript僅僅是用來進行頁面的表單校驗,在低網速時代降低用戶等待服務器響應的時間成本。隨著Web前端技術的發展,雖然前端功能越來越強大,越來越被重視,但是單線程似乎也沒有什么解決不了的問題,相比較而言多線程似乎更加的復雜,所以單線程依舊被沿用至今。
既然JavaScript是單線程,但是在日常開發中總是會有一些比較耗時的任務,比如說定時器,再比如說如今已經標準化的Ajax,JavaScript為了解決這些問題,將自身分為了BOM,DOM,ECMAScript,BOM會幫我們解決這些耗時的任務,稱之為異步任務。
正因為瀏覽器的BOM幫我們處理了異步任務,所以大部分的程序員對異步任務除了會用幾乎一無所知,比如同時有多少異步任務在隊列中?異步是否擁堵等,我們都是沒有辦法直接獲得相關信息的,很多情況下,底層確實也不需要我們關注相關的信息,但如果我們在某些情況下想要相關信息的時候,NodeJS提供了一個Experimental的API供我們使用,也就是async_hooks。為什么是NodeJS呢,因為只有在Node中定時器,http這些異步模塊,才是開發者可以控制的,瀏覽器中的BOM是不被開發者控制的,除非瀏覽器提供對應的API。
async_hooks規則
async_hooks約定每一個函數都會提供一個上下文,我們稱之為async scope,每一個async scope中都有一個 asyncId, 是當前async scope的標志,同一個的async scope中asyncId必然相同。
這在多個異步任務并行的時候,asyncId可以使我們可以很好的區分要監聽的是哪一個異步任務。
asyncId是一個自增的不重復的正整數,程序的第一個asyncId必然是1。
async scope通俗點來說就是一個不能中斷的同步任務,只要是不能中斷的,無論多長的代碼都共用一個asyncId,但如果中間是可以中斷的,比如是回調,比如中間有await,都會創建一個新的異步上下文,也會有一個新的asyncId。
每一個async scope中都有一個triggerAsyncId表示當前函數是由那個async scope觸發生成的;
通過 asyncId 和 triggerAsyncId 我們可以很方便的追蹤整個異步的調用關系及鏈路。
async_hooks.executionAsyncId()用于獲取asyncId,可以看到全局的asyncId是1。
async_hooks.triggerAsyncId()用于獲取triggerAsyncId,目前值為0。
1
2
3
|
const async_hooks = require( 'async_hooks' ); console.log( 'asyncId:' , async_hooks.executionAsyncId()); // asyncId: 1 console.log( 'triggerAsyncId:' , async_hooks.triggerAsyncId()); // triggerAsyncId: 0 |
我們這里使用fs.open打開一個文件,可以發現fs.open的asyncId是7,而fs.open的triggerAsyncId變成了1,這是因為fs.open是由全局調用觸發的,全局的asyncId是1。
1
2
3
4
5
6
7
8
|
const async_hooks = require( 'async_hooks' ); console.log( 'asyncId:' , async_hooks.executionAsyncId()); // asyncId: 1 console.log( 'triggerAsyncId:' , async_hooks.triggerAsyncId()); // triggerAsyncId: 0 const fs = require( 'fs' ); fs.open( './test.js' , 'r' , (err, fd) => { console.log( 'fs.open.asyncId:' , async_hooks.executionAsyncId()); // 7 console.log( 'fs.open.triggerAsyncId:' , async_hooks.triggerAsyncId()); // 1 }); |
異步函數的生命周期
當然實際應用中的async_hooks并不是這樣使用的,他正確的用法是在所有異步任務創建、執行前、執行后、銷毀后,觸發回調,所有回調會傳入asyncId。
我們可以使用async_hooks.createHook來創建一個異步資源的鉤子,這個鉤子接收一個對象作為參數來注冊一些關于異步資源生命周期中可能發生事件的回調函數。每當異步資源被創建/執行/銷毀時這些鉤子函數會被觸發。
1
2
3
4
5
6
|
const async_hooks = require( 'async_hooks' ); const asyncHook = async_hooks.createHook({ init(asyncId, type, triggerAsyncId, resource) { }, destroy(asyncId) { } }) |
目前 createHook 函數可以接受五類 Hook Callbacks 如下:
1.init(asyncId, type, triggerAsyncId, resource)
- init 回調函數一般在異步資源初始化的時候被觸發。
- asyncId: 每一個異步資源都會生成一個唯一性標志
- type: 異步資源的類型,一般都是資源的構造函數的名字。
FSEVENTWRAP, FSREQCALLBACK, GETADDRINFOREQWRAP, GETNAMEINFOREQWRAP, HTTPINCOMINGMESSAGE,
HTTPCLIENTREQUEST, JSSTREAM, PIPECONNECTWRAP, PIPEWRAP, PROCESSWRAP, QUERYWRAP,
SHUTDOWNWRAP, SIGNALWRAP, STATWATCHER, TCPCONNECTWRAP, TCPSERVERWRAP, TCPWRAP,
TTYWRAP, UDPSENDWRAP, UDPWRAP, WRITEWRAP, ZLIB, SSLCONNECTION, PBKDF2REQUEST,
RANDOMBYTESREQUEST, TLSWRAP, Microtask, Timeout, Immediate, TickObject
- triggerAsyncId: 表示觸發當前異步資源被創建的對應的 async scope 的 asyncId
- resource: 代表被初始化的異步資源對象
我們可以通過 async_hooks.createHook 函數來注冊關于每個異步資源在生命周期中發生的 init/before/after/destory/promiseResolve 等相關事件的監聽函數;
同一個 async scope 可能會被調用及執行多次,不管執行多少次,其 asyncId 必然相同,通過監聽函數,我們很方便追蹤其執行的次數及時間及上線文關系;
2.before(asyncId)
before函數一般在 asyncId 對應的異步資源操作完成后準備執行回調前被調用,before回調函數可能被執行多次,由其被回調的次數來決定,使用時這里需要注意。
3.after(asyncId)
after回調函數一般在異步資源執行完回調函數后會立即被調用,如果在執行回調函數的過程中發生未捕獲的異常,after 事件會在觸發 “uncaughtException” 事件后被調用。
4.destroy(asyncId)
當asyncId對應的異步資源被銷毀時調用,有些異步資源的銷毀要依賴垃圾回收機制,所以有些情況下由于內存泄漏的原因,destory事件可能永遠不會被觸發。
5.promiseResolve(asyncId)
當 Promise 構造器中的 resovle 函數被執行時,promiseResolve 事件被觸發。有些情況下,有些 resolve 函數是被隱式執行的,比如 .then 函數會返回一個新的 Promise,這個時候也會被調用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
const async_hooks = require( 'async_hooks' ); // 獲取當前執行上下文的 asyncId const eid = async_hooks.executionAsyncId(); // 獲取觸發當前函數的 asyncId const tid = async_hooks.triggerAsyncId(); // 創建新的AsyncHook實例。所有這些回調都是可選的 const asyncHook = async_hooks.createHook({ init, before, after, destroy, promiseResolve }); // 需要顯示聲明 才能執行 asyncHook.enable(); // 禁止監聽新的異步事件。 asyncHook.disable(); function init(asyncId, type, triggerAsyncId, resource) { } function before(asyncId) { } function after(asyncId) { } function destroy(asyncId) { } function promiseResolve(asyncId) { } |
Promise
promise是比較特殊的一種情況,如果足夠細心init方法中的type中你就會發現其中并沒有PROMISE。如果僅使用ah.executionAsyncId()來獲取Promise的的asyncId的話,是不能取得正確的ID的,只有在添加了實際的hook只后,async_hooks才會給Promise的回調創建asyncId。
換句話說,由于V8對于獲取 asyncId 的執行成本比較高,所以默認情況下,我們是不給 Promise 分配新的 asyncId。
也就是說默認情況下,我們使用promises或者 async/await 時是獲取不到當前上下文正確的asyncId和triggerId。不過沒關系,我們可以通過執行async_hooks.createHook(callbacks).enable()函數強制開啟對Promise分配asyncId。
1
2
3
4
5
6
|
const async_hooks = require( 'async_hooks' ); const asyncHook = async_hooks.createHook({ init(asyncId, type, triggerAsyncId, resource) { }, destroy(asyncId) { } }) |
1
2
3
4
5
|
asyncHook.enable(); Promise.resolve(123).then(() => { console.log(`asyncId ${async_hooks.executionAsyncId()} triggerId ${async_hooks.triggerAsyncId()}`); }); |
另外Promise只會觸發init和promiseResolve鉤子事件函數,而before和after事件的鉤子函數只會在Promise的鏈式調用時被觸發,也就是說只有在.then/.catch函數中生成的Promise時才會被觸發。
1
2
3
4
5
|
new Promise(resolve => { resolve(123); }).then(data => { console.log(data); }) |
可以發現,上面的存在兩個Promise,第一個是new實例化創建的,第二個是then創建的(不明白的可以查看之前的Promise源碼文章)。
這里的順序是執行new Promise的時候會調用自身的init函數,然后在執行resolve的時候調用promiseResolve函數。接著在then方法中執行第二個Promise的init函數,然后執行第二個Promise的before,promiseResovle,after函數。
異常處理
如果注冊的async-hook回調函數中發生異常,那么服務將打印錯誤日志并立即退出,同時所有de 監聽器將被移除,同時會觸發 ‘exit' 事件退出程序。
之所以會立即退出進程,是因為如果這些async-hook 函數運行不穩定,下一個相同事件被觸發時很可能又拋出異常,這些函數主要就是為了監聽異步事件的,如果不穩定應該及時發現并進行更正。
在異步鉤子回調中打印日志
由于 console.log 函數也是一個異步調用,如果我們在 async-hook 函數中再調用 console.log 那么將再次觸發相應的 hook 事件,造成死循環調用,所以我們在 async-hook 函數中必須使用同步打印日志方式來跟蹤,可以使用 fs.writeSync 函數:
1
2
3
4
5
6
|
const fs = require( 'fs' ); const util = require( 'util' ); function debug(...args) { fs.writeFileSync( 'log.out' , `${util.format(...args)}\n`, { flag: 'a' }); } |
[參考文獻-AsyncHooks] (https://nodejs.org/dist/latest-v15.x/docs/api/async_hooks.html)
到此這篇關于Node8中AsyncHooks異步生命周期的文章就介紹到這了,更多相關Node AsyncHooks異步生命周期內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://juejin.cn/post/6950545906181767205