最近在作公司serverless相關需求的時候,須要封裝調用鏈上報的組件,在傳入traceId和userId等上下文信息時,須要從框架層逐層往下傳遞,好比打印一個log,須要這樣:javascript
// 基於koa的某個工具包內部
log.info('這是一個log', req)
複製代碼
全部須要上下文的地方都須要傳入,致使代碼嚴重耦合,咱們有什麼辦法能夠優雅的解決這個問題呢?html
const http = require('http');
//create a server object:
http.createServer(function (req, res) {
logicA(req, res)
}).listen(8080); //the server object listens on port 8080
function logicA(req, res) {
logicA1(req, res)
}
// 無所不在req, res
function logicA1(req, res) {
res.write('Hello World!'); //write a response to the client
res.end(); //end the response
}
複製代碼
無所不在的req res的透傳前端
其餘語言是怎麼處理的呢?java
以java爲例,java中有一個功能叫局部線程存儲(Thread-local Storage)例如在某些網絡模型中好比當一個請求來的時候(本人對java瞭解很少,不詳細展開),程序會在線程池裏分配一個線程去處理這個請求,在這個線程中有局部變量是當前請求線程內共享的,線程內都能訪問的。node
Continuation-local Storage與TLS相似,不過是基於Nodejs風格的回調調用。它得名於函數式編程中的Continuation-passing style,旨在鏈式函數調用過程當中維護一個持久的數據。git
直接講Node可能有的同窗不理解,咱們能夠從瀏覽器舉例。Node web server的一次請求,其實也是一個事件,能夠類比瀏覽器的一次點擊事件。在瀏覽器端,咱們處理複雜邏輯的時候,可能會遇到如下的代碼github
<html>
<header></header>
<body>
<button id="button" />
<script> button.addEventListener('click', event => { logicA(event) // 其餘處理邏輯 }) function logicA(event) { logicA1(event) // 其餘處理邏輯 } // logicA1 maybe in other module function logicA1(event) { console.log(`x: ${event.x}, y: ${event.y}`) } 複製代碼
效果以下web
有同窗可能會想,既然event
無處不在,那我放在全局變量上不就行了?代碼以下
const gloabContext = {}
button.addEventListener('click', event => {
gloabContext.event = event
logicA()
// 其餘處理邏輯
})
function logicA() {
logicA1()
// 其餘處理邏輯
}
// logicA1 maybe in other module
function logicA1() {
console.log(`x: ${gloabContext.event.x}, y: ${gloabContext.event.y}`)
}
複製代碼
globalContext和咱們透傳的event一致,看起來好像也沒啥問題編程
其實問題很大segmentfault
這個例子之因此沒問題,是由於咱們的邏輯函數是同步的,若是咱們加入異步邏輯會發生什麼事呢?
const gloabContext = {}
button.addEventListener('click', event => {
gloabContext.event = event
logicA(event)
// 其餘處理邏輯
})
async function logicA(event) {
await delay(3000)
logicA1(event)
// 其餘處理邏輯
}
// logicA1 maybe in other module
function logicA1(event) {
console.log(`gloabContext x: ${gloabContext.event.x}, y: ${gloabContext.event.y}`)
console.log(`event x: ${event.x}, y: ${event.y}`)
console.log('\n')
}
async function delay(timeout) {
return new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, timeout);
})
}
複製代碼
效果以下:
能夠發現globalContext
中的值已經和咱們指望保存的
event
不一致了。
咱們須要的是一個能夠關聯異步操做的數據,任意的異步操做能夠訪問這些數據,卻互不影響。
Zone.js
是Angular團隊在Angular2中引入的,google團隊Zones能夠幫助開發者作到如下的事情:
把一些數據關聯到 zone 中,相似於某些語言中的本地線程存儲(thread local storage),這樣在 zone 中的任意異步操做均可以訪問這些數據。
自動追蹤指定 zone 還未執行完的異步任務,以便執行相似清理、渲染或者測試斷言等。
分析發生在當前 zone 中異步執行的總時間,用於分析工做。
處理 zone 中全部未捕獲的異常或者未處理的 promise reject,阻斷他們往上層冒泡。
廢話少說,先看用Zone.js如何解決咱們剛纔的問題
button.addEventListener('click', event => {
Zone.current.fork({
name: 'clickZone',
properties: {
event
}
}).run(
() => logicA(event)
)
// 其餘處理邏輯
})
function logicA(event) {
delay(3000).then(() => {
logicA1(event)
})
// 其餘處理邏輯
}
// logicA1 maybe in other module
function logicA1(event) {
console.log(Zone.current.name)
console.log(`gloabContext x: ${Zone.current.get('event').x}, y: ${Zone.current.get('event').y}`)
console.log(`event x: ${event.x}, y: ${event.y}`)
console.log('\n')
}
function delay(timeout) {
return new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, timeout);
})
}
複製代碼
效果以下:
Amazing!
Zone.current.get('event')
與咱們傳遞的event
一致了!並且Zone.js還封裝了Nodejs相關的API,咱們在服務端也能使用。
Zone.js還提供大量的鉤子,有更多強大的用法,好比用來追蹤未完成的異步宏任務和微任務,能夠參考這篇文章翻閱源碼後,我終於理解了Zone.js
不幸的是,沒有。
細心的同窗已經發現,我在使用Zone.js的時候並無使用async
函數,咱們試試改爲async
函數後會發生什麼。代碼以下
button.addEventListener('click', event => {
Zone.current.fork({
name: 'clickZone',
properties: {
event
}}).run( () => logicA(event))
// 其餘處理邏輯
})
async function logicA(event) {
await delay(3000)
logicA1(event)
// 其餘處理邏輯
}
// logicA1 maybe in other module
function logicA1() {
console.log(Zone.current.name)
console.log(`gloabContext x: ${Zone.current.get('event').x}, y: ${Zone.current.get('event').y}`)
console.log(`event x: ${event.x}, y: ${event.y}`)
console.log('n')
}
async function delay(timeout) {
return new Promise(resolve => {
setTimeout(() => {
resolve(true)
}, timeout);
})
}
複製代碼
報錯了,Zone.current.name
也不是咱們的clickZone
,變成了<root>
了,怎麼回事呢?
這和Zone.js的實現有關
Zone.js採用猴子補丁(Monkey-patched)的暴力方式將Javascript中的異步函數都包裝了一層,在瀏覽器中,包括RequestAnimationFrame,addEventListener,XMLHttpRequest等一系列異步方法。在Nodejs裏,全部的異步api也被封裝了一層(google工程師真暴力 = =!;今天的主角不是Zone.js,也就不展開了,有興趣的同窗能夠去查看源碼)。
固然,這裏被改寫的對象也包括咱們的Promise對象。
未引入Zone.js前,咱們打印Promise顯示以下:
引入Zone.js後,打印以下:
能夠看到window.Promise對象已經被修改了,系統原生Promise被放在了__zone_symbol__Promise
。
那爲何咱們用Pormise.then
的方式調用Zone.js
是正常的,用async
函數的形式不行呢?
這又扯到了v8對async函數的實現,在v8中 async
函數返回的結果是一個promise,若是 return 的不是一個promise,也會封裝成一個promise對象,效果以下:
那麼,引入Zone.js後呢,發生了什麼
async
函數返回的promise包裝對象不是全局Promise的實例,而是native的!
更確鑿的證據在這裏
能夠這麼理解v8的行爲:
async 函數的返回值若是不是native的promise,則v8會將其封裝成native的promise,而與js中全局的Promise對象無關。
這麼一來,Zone.js中的Monkey-patched就失效了。(其實關於async await還有一些有意思的東西,若是後面有時間會寫一篇文章講講這裏)
這個Issues裏面也提到了,因爲v8的實現機制,致使zone.js沒法支持async await語法,只有使用babel或者ts將async await編譯成generator的形式。其實angular團隊也早將Zones for JavasSript
提到TC39 process,可是至今是stage 0的狀態,感受但願渺茫。
對於前端來講用babel還能解釋爲兼容瀏覽器版本,但對於Node應用來講,編譯async函數增長了調試的複雜度,那還有什麼解決辦法嗎?
domain模塊早在node v0.8版本的時候就發佈了。這個模塊最先是用於捕捉異步回調中出現的異常,在騰訊開源的TSW中使用了domain來實現保存請求上下文:
經過process.domain始終指向當前執行棧所在的domain以及Object.defineProperty
,實現了全局變量保存執行上下文
domain如今已被node官方標識爲Stability: 0 - Deprecated
(廢棄的)狀態,如今咱們去看domain模塊的源碼能夠發現,該模塊已經用async_hooks重寫了,意味着即便最後從node api中移除,咱們經過async_hooks也能本身實現domain。
在node8.0版本以後引入了async_hooks模塊,該模塊的狀態是Stability: 1 - Experimental
(實驗性的),而且在github上有對async_hooks使用的性能問題的討論,在基於koa框架下,性能損失在10%左右。除了性能損失,還有部分使用者出現了cpu暴漲的狀況,這裏由於信息有限,沒法得知是否和使用者自身的編碼有關。
除了async_hooks模塊有性能損失,domain模塊在基於async_hooks重寫前自身也存在大約15%的性能損失。
阿里的Nodejs應用管理器 Pandora.js 就是用的async_hooks
來作鏈路追蹤的,其源碼裏依賴的cls-hooked包就是基於async_hooks
模塊實現。
Pandora.js源碼,能夠看到使用了
cls-hooked
。
這裏不對async_hooks模塊的使用作過多展開,感興趣的同窗去看看api就知道了。
Zone.js: 支持瀏覽器,Nodejs,沒法直接使用async await語法,須要編譯。
Domain模塊:支持Nodejs,已廢棄,已用async_hooks實現。
Async_hooks模塊: 支持Nodejs,存在性能損耗,可能存在內存泄漏,cpu暴漲的問題。
爲了性能安全,咱們能夠增長一個開關,在必要時候關閉async_hooks
或 domain
的功能,同時作到不影響業務主流程。
寫完下班,最後祝你們多拿年終獎。
angular with tsconfig target ES2017 async/await will not work with zone.js