本篇文章为大家展示了Node.js中怎么使用Hooks实现异步,内容简明扼要并且容易理解,绝对能使你眼前一亮,通过这篇文章的详细介绍希望你能有所收获。
API使用
我总是觉得官方文档过于复杂以及苛刻。这就是为什么我通常会选择传统、友好的博客文章。
让我们首先了解一下Async Hooks API提供的 5 个可用事件函数:
init: 顾名思义,当特定的异步资源初始化时会调用它。仅作记录,此时,我们已经将钩子与异步资源相关联。
before 和 after: 这与普通语言中的函数的执行前和执行后非常相似。在资源执行之前和之后分别调用它们。
destroy: 很明显,无论资源的回调函数发生了什么,只要资源被销毁就会调用它。
promiseResolve: promiseResolve与Promise有关,当你的Promise调用它的 resolve 函数时,挂钩就会触发此函数。
非常的简单直接,接下来让我们看一个基本的例子:
const myFirstAsyncHook = async_hooks.createHook({ init, before, after, destroy, promiseResolve });
是的,你必须先创建每个事件函数,然后再将其分配给createHook函数。另外,必须显式启用该挂钩:
myFirstAsyncHook.enable();
让我们继续看一个更加完整的例子:
const fs = require("fs"); const async_hooks = require("async_hooks"); // Sync write to the console const writeSomething = (phase, more) => { fs.writeSync( 1, `Phase: "${phase}", Exec. Id: ${async_hooks.executionAsyncId()} ${ more ? ", " + more : "" }\n` ); }; // Create and enable the hook const timeoutHook = async_hooks.createHook({ init(asyncId, type, triggerAsyncId) { writeSomething( "Init", `asyncId: ${asyncId}, type: "${type}", triggerAsyncId: ${triggerAsyncId}` ); }, before(asyncId) { writeSomething("Before", `asyncId: ${asyncId}`); }, destroy(asyncId) { writeSomething("Destroy", `asyncId: ${asyncId}`); }, after(asyncId) { writeSomething("After", `asyncId: ${asyncId}`); }, }); timeoutHook.enable(); writeSomething("Before call"); // Set the timeout setTimeout(() => { writeSomething("Exec. Timeout"); }, 1000);
这个例子通过众所周知的原生函数 setTimeout 去追踪超时的异步执行过程。
在我们深入研究之前,先快速浏览一下第一个函数 writeSomething 。你也许很好奇为什么在我们已经有函数可以在控制台输出的情况下仍然创建了一个新的函数去完成相同的功能。
原因是你不能使用任何 console 函数去测试异步钩子,因为它们本身就是异步的。因此当我们在下面提供了一个 init 函数时,它会产生一个无限循环。该函数会调用 console 的 log ,此日志又会再次触发初始化,以此类推,陷入死循环。
这就是为什么我们需要重新写一个“同步”日志功能。
好了,现在我们回过头去看代码。我们的异步钩子提供了四个功能:init、 before、 after 以及 destory。而且,我们还在超时之前和执行期间打印一条消息,所以你可以看到整个过程是如何线性进行的。
在你的命令行执行 node index.js,你会得到如下图所示的结果:
观察下钩子是如何一步一步执行追踪的。看起来是一种很有趣的跟踪方式,尤其是当你考虑将数据输入到监视工具中或者是你已经使用的日志追踪工具。
一个Promise例子
让我们看看我们的示例在Promise下的执行效果。思考下面这些代码片段:
const calcPow = async(n, exp) => { writeSomething("Exec. Promise"); return Math.pow(n, exp); }; (async() => { await calcPow(3, 4); })();
你也可以用之前的 setTimeout 示例来替代这个例子。在这段代码中,我们有一个异步函数用来进行幂运算。同时也有一个相同的函数在异步块中被调用。到目前为止,Node.js创建了两个Promise。
下图是日志记录的结果:
奇怪的是,我们有两个Promise,却调用了三次 init 函数。不用担心,这是因为Node.js团队在版本12中引入了异步执行性能方面的一些最新改进。你可以点击此处[4]了解更多信息。
尽管如此,执行过程依然符合我们的预期。
解析:钩子函数的性能与度量
Node.js提供的另一个非常有趣的API是性能评估API[5],既然我们在这里讨论度量,为什么不结合两者的功能来了解我们可以收获什么呢?
可以通过 perf_hooks 获得该API,该API让我们能够用与W3C Web Performance API[6]相似的方式来获得性能/用户时间轴指标。
将它与异步钩子相结合我们可以做一些事情,比如追踪异步函数执行完毕需要的时间。让我们看另外一个例子:
const async_hooks = require("async_hooks"); const { performance, PerformanceObserver } = require("perf_hooks"); const hook = async_hooks.createHook({ init(asyncId) { performance.mark(`init-${asyncId}`); }, destroy(asyncId) { performance.mark(`destroy-${asyncId}`); performance.measure( `entry-${asyncId}`, `init-${asyncId}`, `destroy-${asyncId}` ); }, }); hook.enable(); const observer = new PerformanceObserver((data) => console.log(data.getEntries()) ); observer.observe({ entryTypes: ["measure"], buffered: true }); setTimeout(() => { console.log("I'm a timeout"); }, 1200);
既然我们只是追踪记录执行时间,就没有必要用之前用的中间事件函数。用 init 和 destroy 就足够了。
就像异步钩子那样,性能API通过创建观察者来工作。不过,无论什么时候开始或者结束,你都必须明确标记每个事件的id。这样,当我们调用API的 measure 函数时,它将汇总收集到的数据并将其立即发送给观察者,观察者将为我们记录全部的日志。
注意了,这里我们使用了两次 console.log 函数。第一次是无影响的因为它包含在观察者中执行。但是第二次它在 setTimeout 函数中执行,另一个异步中的异步,这意味着在最后它会产生不同的输出。
下图是日志记录:
本示例本并没有考虑事件类型之间的差异。在这里,我们在同一测量场景中发生了超时和异步日志操作。
但是,考虑到生产环境,建议你创建一个更强大的机制在每次调用 init 时存储事件类型,并在稍后调用 destroy 函数,倒霉的没有接收到参数类型时检查存储是否依然存在。
异步资源
Async Hooks中的另一个有用功能是 `AsyncResource`[7] 类。每当你为框架或库创建自己的资源时,它都会为你提供帮助。
只需输入以下代码即可使用:
const AsyncResource = require('async_hooks').AsyncResource;
用这种方式,你可以使用它实例化一个新对象,并手动定义其每个阶段在整个代码中何时开始。举个例子:
const resource = new AsyncResource('MyOwnResource'); someFunction(function someCallback() { resource.emitBefore(); // do your stuff... resource.emitAfter(); }); someOnClose() { resource.emitDestroy(); }
上述内容就是Node.js中怎么使用Hooks实现异步,你们学到知识或技能了吗?如果还想学到更多技能或者丰富自己的知识储备,欢迎关注编程网行业资讯频道。