最近在作公司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