const asyncHooks = require("async_hooks"); const { executionAsyncId } = asyncHooks; // 保存异步调用的上下文。 const contexts = {}; const hooks = asyncHooks.createHook({ // 对象构造时会触发 init 事件。 init: function(asyncId, type, triggerId, resource) { // triggerId 即为当前函数的调用者的 asyncId 。 if (contexts[triggerId]) { // 设置当前函数的异步上下文与调用者的异步上下文一致。 contexts[asyncId] = contexts[triggerId]; } }, // 在销毁对象后会触发 destroy 事件。 destroy: function(asyncId) { if (!contexts[asyncId]) return; // 销毁当前异步上下文。 delete contexts[asyncId]; } }); // 关键!允许该实例中对异步函数启用 hooks 。 hooks.enable(); // 模拟业务处理函数。 function handler(params) { // 设置 context ,可在中间件中完成此操作(如 Logger Middleware)。 contexts[executionAsyncId()] = params; // 以下是业务逻辑。 console.log(`handler ${JSON.stringify(params)}`); f(); } function f() { setTimeout(() => { // 输出所属异步过程的 params 。 console.log(`setTimeout ${JSON.stringify(contexts[executionAsyncId()])}`); }); } // 模拟两个异步过程(两个请求)。 setTimeout(handler, 0, { id: 0 }); setTimeout(handler, 0, { id: 1 });
在上述代码中,我们先声明了 contexts 用于存储每个异步过程中的上下文数据(如 Trace ID),随后我们创建了一个 Async Hooks 实例。我们在异步资源初始化时,设置当前 Async ID 对应的上下文数据,使得其数据为调用者的上下文数据;我们在异步资源被销毁时,删除其对应的上下文数据。
通过这种方式,我们只需在一开始设置上下文数据,即可在其引发的各个过程(同步和异步过程)中,获得上下文数据,从而解决了问题。
执行上述代码,其运行结果如下。根据输出日志可知,我们的解决方案是可行的。
handler {"id":0} handler {"id":1} setTimeout {"id":0} setTimeout {"id":1}
不过需要注意的是,Async Hooks 是 实验性 API , 存在一定的性能损耗 ,但 Node 官方正努力将其变得生产可用。因此, 在机器资源足够的情况下,使用本解决方案,牺牲部分性能,换取开发体验。