Node8中AsyncHooks異步生命周期
Async Hooks 是 Node8 新出來的特性,提供了一些 API 用于跟蹤 NodeJs 中的異步資源的生命周期,屬于 NodeJs 內(nèi)置模塊,可以直接引用。
const async_hooks = require('async_hooks');
這是一個很少使用的模塊,為什么會有這個模塊呢?
我們都知道,JavaScript在設(shè)計之初就是一門單線程語言,這和他的設(shè)計初衷有關(guān),最初的JavaScript僅僅是用來進行頁面的表單校驗,在低網(wǎng)速時代降低用戶等待服務(wù)器響應(yīng)的時間成本。隨著Web前端技術(shù)的發(fā)展,雖然前端功能越來越強大,越來越被重視,但是單線程似乎也沒有什么解決不了的問題,相比較而言多線程似乎更加的復(fù)雜,所以單線程依舊被沿用至今。
既然JavaScript是單線程,但是在日常開發(fā)中總是會有一些比較耗時的任務(wù),比如說定時器,再比如說如今已經(jīng)標準化的Ajax,JavaScript為了解決這些問題,將自身分為了BOM,DOM,ECMAScript,BOM會幫我們解決這些耗時的任務(wù),稱之為異步任務(wù)。
正因為瀏覽器的BOM幫我們處理了異步任務(wù),所以大部分的程序員對異步任務(wù)除了會用幾乎一無所知,比如同時有多少異步任務(wù)在隊列中?異步是否擁堵等,我們都是沒有辦法直接獲得相關(guān)信息的,很多情況下,底層確實也不需要我們關(guān)注相關(guān)的信息,但如果我們在某些情況下想要相關(guān)信息的時候,NodeJS提供了一個Experimental的API供我們使用,也就是async_hooks。為什么是NodeJS呢,因為只有在Node中定時器,http這些異步模塊,才是開發(fā)者可以控制的,瀏覽器中的BOM是不被開發(fā)者控制的,除非瀏覽器提供對應(yīng)的API。
async_hooks規(guī)則
async_hooks約定每一個函數(shù)都會提供一個上下文,我們稱之為async scope,每一個async scope中都有一個 asyncId, 是當(dāng)前async scope的標志,同一個的async scope中asyncId必然相同。
這在多個異步任務(wù)并行的時候,asyncId可以使我們可以很好的區(qū)分要監(jiān)聽的是哪一個異步任務(wù)。
asyncId是一個自增的不重復(fù)的正整數(shù),程序的第一個asyncId必然是1。
async scope通俗點來說就是一個不能中斷的同步任務(wù),只要是不能中斷的,無論多長的代碼都共用一個asyncId,但如果中間是可以中斷的,比如是回調(diào),比如中間有await,都會創(chuàng)建一個新的異步上下文,也會有一個新的asyncId。
每一個async scope中都有一個triggerAsyncId表示當(dāng)前函數(shù)是由那個async scope觸發(fā)生成的;
通過 asyncId 和 triggerAsyncId 我們可以很方便的追蹤整個異步的調(diào)用關(guān)系及鏈路。
async_hooks.executionAsyncId()用于獲取asyncId,可以看到全局的asyncId是1。
async_hooks.triggerAsyncId()用于獲取triggerAsyncId,目前值為0。
const async_hooks = require('async_hooks'); console.log('asyncId:', async_hooks.executionAsyncId()); // asyncId: 1 console.log('triggerAsyncId:', async_hooks.triggerAsyncId()); // triggerAsyncId: 0
我們這里使用fs.open打開一個文件,可以發(fā)現(xiàn)fs.open的asyncId是7,而fs.open的triggerAsyncId變成了1,這是因為fs.open是由全局調(diào)用觸發(fā)的,全局的asyncId是1。
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 });
異步函數(shù)的生命周期
當(dāng)然實際應(yīng)用中的async_hooks并不是這樣使用的,他正確的用法是在所有異步任務(wù)創(chuàng)建、執(zhí)行前、執(zhí)行后、銷毀后,觸發(fā)回調(diào),所有回調(diào)會傳入asyncId。
我們可以使用async_hooks.createHook來創(chuàng)建一個異步資源的鉤子,這個鉤子接收一個對象作為參數(shù)來注冊一些關(guān)于異步資源生命周期中可能發(fā)生事件的回調(diào)函數(shù)。每當(dāng)異步資源被創(chuàng)建/執(zhí)行/銷毀時這些鉤子函數(shù)會被觸發(fā)。
const async_hooks = require('async_hooks'); const asyncHook = async_hooks.createHook({ init(asyncId, type, triggerAsyncId, resource) { }, destroy(asyncId) { } })
目前 createHook 函數(shù)可以接受五類 Hook Callbacks 如下:
1.init(asyncId, type, triggerAsyncId, resource)
- init 回調(diào)函數(shù)一般在異步資源初始化的時候被觸發(fā)。
- asyncId: 每一個異步資源都會生成一個唯一性標志
- type: 異步資源的類型,一般都是資源的構(gòu)造函數(shù)的名字。
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: 表示觸發(fā)當(dāng)前異步資源被創(chuàng)建的對應(yīng)的 async scope 的 asyncId
- resource: 代表被初始化的異步資源對象
我們可以通過 async_hooks.createHook 函數(shù)來注冊關(guān)于每個異步資源在生命周期中發(fā)生的 init/before/after/destory/promiseResolve 等相關(guān)事件的監(jiān)聽函數(shù);
同一個 async scope 可能會被調(diào)用及執(zhí)行多次,不管執(zhí)行多少次,其 asyncId 必然相同,通過監(jiān)聽函數(shù),我們很方便追蹤其執(zhí)行的次數(shù)及時間及上線文關(guān)系;
2.before(asyncId)
before函數(shù)一般在 asyncId 對應(yīng)的異步資源操作完成后準備執(zhí)行回調(diào)前被調(diào)用,before回調(diào)函數(shù)可能被執(zhí)行多次,由其被回調(diào)的次數(shù)來決定,使用時這里需要注意。
3.after(asyncId)
after回調(diào)函數(shù)一般在異步資源執(zhí)行完回調(diào)函數(shù)后會立即被調(diào)用,如果在執(zhí)行回調(diào)函數(shù)的過程中發(fā)生未捕獲的異常,after 事件會在觸發(fā) “uncaughtException” 事件后被調(diào)用。
4.destroy(asyncId)
當(dāng)asyncId對應(yīng)的異步資源被銷毀時調(diào)用,有些異步資源的銷毀要依賴垃圾回收機制,所以有些情況下由于內(nèi)存泄漏的原因,destory事件可能永遠不會被觸發(fā)。
5.promiseResolve(asyncId)
當(dāng) Promise 構(gòu)造器中的 resovle 函數(shù)被執(zhí)行時,promiseResolve 事件被觸發(fā)。有些情況下,有些 resolve 函數(shù)是被隱式執(zhí)行的,比如 .then 函數(shù)會返回一個新的 Promise,這個時候也會被調(diào)用。
const async_hooks = require('async_hooks'); // 獲取當(dāng)前執(zhí)行上下文的 asyncId const eid = async_hooks.executionAsyncId(); // 獲取觸發(fā)當(dāng)前函數(shù)的 asyncId const tid = async_hooks.triggerAsyncId(); // 創(chuàng)建新的AsyncHook實例。所有這些回調(diào)都是可選的 const asyncHook = async_hooks.createHook({ init, before, after, destroy, promiseResolve }); // 需要顯示聲明 才能執(zhí)行 asyncHook.enable(); // 禁止監(jiān)聽新的異步事件。 asyncHook.disable(); function init(asyncId, type, triggerAsyncId, resource) { } function before(asyncId) { } function after(asyncId) { } function destroy(asyncId) { } function promiseResolve(asyncId) { }
Promise
promise是比較特殊的一種情況,如果足夠細心init方法中的type中你就會發(fā)現(xiàn)其中并沒有PROMISE。如果僅使用ah.executionAsyncId()來獲取Promise的的asyncId的話,是不能取得正確的ID的,只有在添加了實際的hook只后,async_hooks才會給Promise的回調(diào)創(chuàng)建asyncId。
換句話說,由于V8對于獲取 asyncId 的執(zhí)行成本比較高,所以默認情況下,我們是不給 Promise 分配新的 asyncId。
也就是說默認情況下,我們使用promises或者 async/await 時是獲取不到當(dāng)前上下文正確的asyncId和triggerId。不過沒關(guān)系,我們可以通過執(zhí)行async_hooks.createHook(callbacks).enable()函數(shù)強制開啟對Promise分配asyncId。
const async_hooks = require('async_hooks'); const asyncHook = async_hooks.createHook({ init(asyncId, type, triggerAsyncId, resource) { }, destroy(asyncId) { } })
asyncHook.enable(); Promise.resolve(123).then(() => { console.log(`asyncId ${async_hooks.executionAsyncId()} triggerId ${async_hooks.triggerAsyncId()}`); });
另外Promise只會觸發(fā)init和promiseResolve鉤子事件函數(shù),而before和after事件的鉤子函數(shù)只會在Promise的鏈式調(diào)用時被觸發(fā),也就是說只有在.then/.catch函數(shù)中生成的Promise時才會被觸發(fā)。
new Promise(resolve => { resolve(123); }).then(data => { console.log(data); })
可以發(fā)現(xiàn),上面的存在兩個Promise,第一個是new實例化創(chuàng)建的,第二個是then創(chuàng)建的(不明白的可以查看之前的Promise源碼文章)。
這里的順序是執(zhí)行new Promise的時候會調(diào)用自身的init函數(shù),然后在執(zhí)行resolve的時候調(diào)用promiseResolve函數(shù)。接著在then方法中執(zhí)行第二個Promise的init函數(shù),然后執(zhí)行第二個Promise的before,promiseResovle,after函數(shù)。
異常處理
如果注冊的async-hook回調(diào)函數(shù)中發(fā)生異常,那么服務(wù)將打印錯誤日志并立即退出,同時所有de 監(jiān)聽器將被移除,同時會觸發(fā) ‘exit' 事件退出程序。
之所以會立即退出進程,是因為如果這些async-hook 函數(shù)運行不穩(wěn)定,下一個相同事件被觸發(fā)時很可能又拋出異常,這些函數(shù)主要就是為了監(jiān)聽異步事件的,如果不穩(wěn)定應(yīng)該及時發(fā)現(xiàn)并進行更正。
在異步鉤子回調(diào)中打印日志
由于 console.log 函數(shù)也是一個異步調(diào)用,如果我們在 async-hook 函數(shù)中再調(diào)用 console.log 那么將再次觸發(fā)相應(yīng)的 hook 事件,造成死循環(huán)調(diào)用,所以我們在 async-hook 函數(shù)中必須使用同步打印日志方式來跟蹤,可以使用 fs.writeSync 函數(shù):
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)
到此這篇關(guān)于Node8中AsyncHooks異步生命周期的文章就介紹到這了,更多相關(guān)Node AsyncHooks異步生命周期內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Node.js高級編程cluster環(huán)境及源碼調(diào)試詳解
這篇文章主要為大家介紹了Node.js高級編程cluster環(huán)境及源碼調(diào)試詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-12-12nodejs body-parser 解析post數(shù)據(jù)實例
下面小編就為大家?guī)硪黄猲odejs body-parser 解析post數(shù)據(jù)實例。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-07-07node.js中的events.EventEmitter.listenerCount方法使用說明
這篇文章主要介紹了node.js中的events.EventEmitter.listenerCount方法使用說明,本文介紹了events.EventEmitter.listenerCount的方法說明、語法、使用實例和實現(xiàn)源碼,需要的朋友可以參考下2014-12-12