1. 前言
本篇文章主要是讲解 Node.js 的 API AsyncLocalStorage和AsyncResource。先讲解AsyncLocalStorage,再讲解AsyncResource。
同时我们会讲如何通过这两个 API 实现异步上下文追踪。
对应的 Node.js 官方文档是 Asynchronous context tracking
本篇文章会简单讲一些 Node.js 的内部原理,因为如果不基于内部原理,要理解这两个 API 会非常困难,所以如果你对这两个 API 比较陌生,那先不要着急去看他们的具体用法,我们先从概念开始。
2. AsyncLocalStorage 简介
AsyncLocalStorage 是一个类,从名字可以看出它是用来做异步局部存储的。
所谓局部,就是在它所限定的执行上下文范围内。 所谓异步,就是这个限定的执行上下文也包括异步操作。 所谓存储,就是可以在内存中存储数据,只有局部的执行上下文范围内可以访问。
以上简介可能比较晦涩。不要着急,我们慢慢的解释。
上面多次提到上下文,下面咱们有必要解释一下什么是上下文。
3. 什么是上下文
举例如下:
js
const a = 9;
function f() {
console.log(a);
}
function x() {
let a = 11;
f();
}
x();
上面这段 JavaScript 代码中,console.log(a)会打印什么呢?是9。
例子中,f函数的定义处和执行处分别定义了a变量,那这两个a变量,一个是f函数定义时的上下文,其值是9,另一个则是f函数执行时的上下文,其值是11。
上下文其实就是某段代码所在的语境,定义f函数的语境中,有一个值是9的变量a。执行f函数的语境中,有一个值是11的变量a。
定义f函数所在的语境,我们把它叫做f函数的词法上下文(Lexical Context)。 f函数执行时的所在的语境,我们把它叫做f函数的执行上下文(Execution Context)。
上面例子中最终打印的是9。说明f函数内部只能获取到词法上下文的变量,而不能获取到执行上下文的变量。
试想如果函数的执行上下文可以影响函数内部的变量的话会发生什么:
一个函数的使用者定义了一个变量正好和函数内部的一个变量重名了,函数内部变量的值被覆盖,函数运行出错。函数的使用者为了不出现这种情况,需要仔细的查看函数的源码,以防止所定义的变量和函数内部变量重名。这是一件多恐怖的事情。
因此 JavaScript 和大部分编程语言一样,函数内部变量的值是由其词法上下文决定的,而不是执行上下文。
看到这里大家应该明白什么是"上下文"(Context)了。
4. 异步上下文追踪
4.1 抛出问题
AsyncLocalStorage AsyncResource最典型的应用就是上下文追踪。
举例如下: 做服务端开发时,一旦线上出现 bug ,我们往往需要通过日志来找线索,这时往往需要对某个请求进行追踪,但是线上的请求往往会很多,很多请求的日志混在一起,要想捋清单个请求的日志会比较困难。
对于这种问题我们通常的做法是给每个请求都加上id,从客户端到服务端都加上,这样就可以捋清每个请求的执行情况。
在依靠多线程进行并发处理的服务端架构中,这种问题比较好解决,像 Java 有 ThreadLocal 的概念,每个线程都可以维护自己的全局变量,线程之间是相互隔离的,而每个线程代表一个请求,因此只需要在ThreadLocal下存入id,任何位置的代码都可以访问到该id。
而 Node.js 中,所有请求都是由同一个线程接手,然后一些耗时的操作由 Node.js 内部在后台进行多线程的处理,处理好后再将数据排队返回给那个单一线程。应用程序的业务逻辑代码大部分时候都是在同一个线程中执行。
那我们怎么才能做到像 Java 那样对每个请求都维护相互隔离的全局变量呢?或者说维护一个请求级别的上下文呢?
下面的我们用for循环来模拟海量的请求,for循环中的requestId作为用于追踪请求的id。 代码举例如下:
js
function f() {
x();
}
function x() {
// 希望能获取到 requestId ,但是报错了:Uncaught ReferenceError: requestId is not defined
console.log(requestId);
}
// for 循环模拟海量的 Http 请求
for(let requestId = 0; requestId < 10; requestId++) {
// 海量请求的处理函数
f();
}
由于在词法上下文中找不到requestId。例子中x函数无法获取requestId的值,报错:Uncaught ReferenceError: requestId is not defined。
要获取到requestId,我们有以下几种解决方案。
4.2 方案 1
js
// for 循环模拟海量的 Http 请求
for(let requestId = 0; requestId < 10; requestId++) {
const x = function () {
console.log(requestId);
}
const f = function () {
x();
}
f();
}
该方案需要把f和x都放到for循环内部,这显然不是好的解决方案,x和f被重复定义多次,况且作为服务端的应用,我们把所有代码都放到一起,不分模块,那将会导致维护灾难。
4.3 方案 2
js
function f(requestId) {
x(requestId);
}
function x(requestId) {
console.log(requestId);
}
// for 循环模拟海量的 Http 请求
for(let requestId = 0; requestId < 10; requestId++) {
f(requestId);
}
该方案需要在每个函数中都传入requestId参数。显然也不是一个好的方案,requestId不是业务逻辑的一部分,它是一个辅助性的监测逻辑,让每个函数都传入requestId,与真正业务逻辑的耦合就太高了,会给业务开发带来沉重的负担,且容易出错,成本较高。
JavaScript 语言自带的解决方案都无法很好的解决该问题。
4.4 async_hooks 方案
这是 Node.js 提出的第一个原生的方案。
在讲具体方案前,我们先了解一下什么是异步任务。
4.4.1 Node.js 异步任务的原理
Node.js 内部执行 JavaScript 代码是分成若干任务执行的,例如setTimeout的回调函数就是一个单独的任务,在浏览器中我们称为宏任务。
其实 JavaScript 引擎 V8并不知道宏任务的存在,因为像setTimeout这种函数其实并不是 JavaScript 语言的一部分,ECMAScript 标准中并不存在setTimeout的标准。在Promise出现之前,V8 只是顺序的执行代码,根本没有异步的概念,直到Promise被纳入 JavaScript 才有了微任务的异步概念。 因此Promise之外的异步功能其实都是 Node.js 自己实现的,这一点和浏览器是一样的。
Node.js 把像setTimeout这样的异步功能分成两段同步的代码,分批交给 V8 执行,一批是执行setTimeout的那部分代码,另一批是setTimeout回调函数所执行的代码,当然也包括其调用的外部代码。这两个批就是两个异步任务。因此每执行一个异步操作,就单独分出一个异步任务,这些异步任务按照一定的优先级规则排队喂给 V8 执行。
这就是异步任务的概念。
4.4.2 async_hooks 方案
现在终于可以讲async_hooks方案了。
async_hooks方案给每个异步任务都分配一个id,叫做asyncId,同时告知每个异步任务的父任务的id,叫做triggerId。另外它也可以把Pomise也作为异步任务,给其分配id,由于Promise是 V8 维护的,相互之间衔接不是很流畅,导致有一些性能问题,因此默认情况下是不会给Promise分配id的,需要显式地调用createHook,且调用enable来强制开启该功能。
有了给异步任务分配id这个功能开发人员就可以通过异步任务的id和其父任务id将所有异步任务都连串起来,整个异步任务的关系是一个树状结构。开发人员可以通过全局的Map 将 id与要存储的数据都对应起来。
这种对应的方式可以有多种,这里列举两个最容易想到的方式。
一种是在全局的Map对象中维护asyncId与requestId的对应关系,但是只里面的asyncId只来自于最初承接 Http 请求的那个异步任务的asyncId,当其他子异步任务像获取request时,只能通过往上一层一层找triggerId,顺藤摸瓜找到最初始异步任务的asyncId,拿着该asyncId从Map中找到requestId。
这种方式访问requestId会比较慢,因为每次都需要进行树的遍历。
另一种是在全局的Map对象中维护每个异步任务的asyncId与requestId的对应关系,每次新异步任务产生时,都将父异步任务的asyncId所对应的reqeustId拷一份,和当前asyncId对应起来存入Map。其后所有代码只要拿着自己的asyncId就可以直接从Map中找到requestId。
相比于第一种方式,这种方式访问requestId会比较快,但是会存入重复的数据,算是用空间换时间。
相比之下,后一种方式可能更合适一些。事实上 Node.js 不只是给每个异步任务分配了asyncId,还给每个任务分配了一个对象,用于存储该异步任务相关的数据,这个对象叫做resource,也可以叫做AsyncResource。它其实就可以当作一个异步任务的执行上下文。所有异步任务的代码中都可以通过async_hooks的 API executionAsyncResource()方法来获取到自己所在异步任务的resource。这样也就不需要一个全局的Map了,开发人员每次创建异步任务时,只需要将requestId放到新异步任务的resource对象中就可以了,后面如果想获取requestId,只需通过executionAsyncResource()方法拿到当前的resource,从而就可以获取到requestId了。
上面提到开发人员需要在每次创建新的异步任务时对resource进行维护,这是一个比较麻烦的事情,不太可能每次都去写这些代码,即使封装成函数也需要每次创建异步任务时调用一下,例如每次setTimeout时都要在setTimeout附近某个地方维护resource。因此async_hooks提供了一个回调 Hook 的方式:createHook({init, before, after, destroy, promiseResolve}),每次有新的异步任务的创建可能时,init函数就会被触发,init函数会传入即将要创建的新异步任务的resource。这样开发人员就不用到处贴代码了。
这就是 Node.js 最初的async_hooks方案。
4.4.3 async_hooks 方案的问题
该方案有些问题。
首先它过于底层了,需求只是说需要一个像ThreadLocal一样可以存储按照 Http 请求进行隔离的全局数据。有这样一个 API 该给开发人员直接用就可以了。而该方案还需要对其进行封装才可以使用,而createHook本身似乎也没有其他用处。
其次,它有性能问题,每次有新的异步任务都会触发createHook的回调,Node.js 的异步任务非常之多,例如console.log就会创建异步任务,每次console.log都要触发createHook,开发人员对createHook稍有滥用将必然导致 Node.js 的整体性能下降。
因此 Node.js 最初的async_hooks方案已经被 Node.js 官方抛弃了。目前async_hooks.createHook``async_hooks.executionAsyncResource这些 API 都是 Node.js 官方严重不推荐使用的 API。官方说它们有使用方面的问题、安全风险、性能问题。
我们之所以不用代码举具体的例子,就是因为这套最初的方案已经被抛弃,没有必要花太多精力去学习了。了解一下就可以了。
终于我们今天的主角要出场了。
4.5 AsyncLocalStorage 方案
async_hooks最初的那套方案也不是一无是处,它只是太底层了,且容易有性能问题,与其大家都在自己去封装,还不如官方基于这套方案进行封装,大家直接拿过去用就可以了。
于是 Node.js 基于原先的方案推出了AsyncLocalStorage。
AsyncLocalStorage和 Java 的ThreadLocal是不是名字上很像呢?它们确实都在完成类似的目标。
为什么要加个Async呢?因为它要让各个异步任务都能访问LocalStorage,这里的LocalStorage是指某段执行上下文的局部存储。
我们下面就来看看AsyncLocalStorage是怎么解决上面的问题的。
js
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
// for 循环模拟海量的 Http 请求
for(let requestId = 0; requestId < 10; requestId++) {
asyncLocalStorage.run(requestId, () => {
f();
// 成功获取到 requestId
console.log('requestId', asyncLocalStorage.getStore());
});
}
function f() {
x();
// 成功获取到 requestId
console.log('requestId', asyncLocalStorage.getStore());
}
function x() {
// 成功获取到 requestId
console.log('requestId', asyncLocalStorage.getStore());
setTimeout(() => {
// 成功获取到 requestId
console.log('requestId', asyncLocalStorage.getStore());
}, 1000);
}
上面代码中所有由asyncLocalStorage.run的回调函数内部和其调用的外部函数都可以通过getStore获取到run的第一个参数requestId。
下面我们详细介绍AsyncLocalStorage的具体用法。
5. AsyncLocalStorage 详细介绍
5.1 描述
AsyncLocalStorage是一个类。由于它是async_hooks初始方案的延伸,因此它还在async_hooks模块下,需要像如下方式一样引入:
js
const { AsyncLocalStorage } = require('node:async_hooks');
它的实例创建方法如下,构造函数没有参数:
js
new AsyncLocalStorage();
它有若干的静态成员方法和实例成员方法。
AsyncLocalStorage于 Node.js v13.10.0 和 v12.17.0 版首次提出,到 v16.4.0 版本被标记为稳定。但是到目前为止只是核心的几个 API 被标记为了稳定,几个辅助功能的 API 还处于Experimental阶段。
我们只讲解稳定的 API。
5.2 AsyncLocalStorage.prototype.run(store, callback[, ...args])
5.2.1 描述
经过上面的例子相信大家已经大概了解了run方法。
run是AsyncLocalStorage实例的核心方法。
run方法以同步的方式执行一个回调函数,该回调函数中的代码和其调用的外部函数均可以访问run的第一个参数store。
run方法其实创建了一个局部的执行上下文,只有这个局部执行上下文的中的代码才可以访问store。
5.2.2 API 状态
Stable,稳定状态,可以在生产环境使用。
5.2.3 run 的参数
参数store: 任意类型。store中的数据可以由callback内部和其调用的外部函数访问。我们例子中一直用requestId做store,其实store可以是包括对象在内的任意类型。
参数callback: 函数类型。该函数会被同步执行,参数由callback后面的run参数指定。返回值将会作为run返回值。
参数..args: 任意数量的参数,这些参数会被传给callback。
5.2.4 run 的返回值
run 的返回值和 callback 的返回值一致。
5.2.5 关键知识点
run会同步执行callback函数。callback内部和其调用的外部函数中都可以获取store数据,获取方式是AsyncLocalStorage的实例方法getStore。callback的返回值就是run的返回值。callback函数的参数来自于run方法中callback后面的参数。
我们分别举例说明。
5.2.6 知识点 1
run会同步执行callback函数。
js
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
asyncLocalStorage.run({}, () => {
console.log('inner run');
});
console.log('after run');
// 依次打印
// inner run
// after run
上面的例子会先打印inner run,然后打印after run。 可见run的回调函数是同步执行的。 其实run只是为了封装一段代码,来形成一段独特的局部执行上下文环境,该环境可以通过getStore来获取run的第一个参数store的值。
5.2.7 知识点 2
callback内部和其调用的外部函数中都可以获取store数据,获取方式是AsyncLocalStorage的实例方法getStore。
js
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
for(let requestId = 0; requestId < 3; requestId++) {
asyncLocalStorage.run(requestId, () => {
f();
// 回调函数内部,成功获取到 requestId
console.log('requestId', asyncLocalStorage.getStore());
});
}
function f() {
x();
// 内嵌函数调用内部,成功获取到 requestId
console.log('requestId', asyncLocalStorage.getStore());
}
function x() {
// 多层内嵌函数内部,成功获取到 requestId
console.log('requestId', asyncLocalStorage.getStore());
setTimeout(() => {
// 异步的内嵌函数内部,成功获取到 requestId
console.log('requestId in setTimeout', asyncLocalStorage.getStore());
}, 100);
}
// 无法获取 requestId 值,getStore 返回 undefined
console.log('requestId', asyncLocalStorage.getStore());
可以看到只要在回调函数内,均可以获取到 requestId值,包括内嵌的同步函数调用和内嵌异步函数调用。 run函数所创造的执行上下文外部是无法获取到store的。
5.2.8 知识点 3
callback的返回值就是run的返回值。
js
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
const data = asyncLocalStorage.run({}, () => {
return 'aaa';
});
// 打印 aaa
console.log(data);
例子中可以看出,run的回调函数的返回值就是run函数的返回值。
我们其实可以当run不存在,把run的回调函数当作一个特殊的自执行函数。上面的例子可以看作像下面这样:
js
const data = (() => {
return 'aaa';
})();
// 打印 aaa
console.log(data);
只是该自执行函数有些特殊,可以访问store。
因此,如果该函数是一个异步函数,则可以像下面这样:
js
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
(async function() {
const data = await asyncLocalStorage.run({}, async () => {
const data = await fetch('https://xxx');
return data;
});
console.log(data);
})();
我们可以当asyncLocalStorage.run不存在。
5.2.9 知识点 4
callback函数的参数来自于run方法中callback后面的参数。
js
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
asyncLocalStorage.run({}, (a, b, c) => {
// 打印 8 9 10
console.log(a, b, c);
}, 8, 9, 10);
a b c分别来自于 8 9 10。
5.3 AsyncLocalStorage.prototype.getStore()
它是AsyncLocalStorage的实例方法,用于获取run函数设置的store。
上面讲run的部分已经讲过了如何通过getStore获取store的值。这里不再赘述。
5.3.1 API 状态
Stable,稳定状态,可以在生产环境使用。
5.4 其他 API
AsyncLocalStorage的核心 API就是上面的run和getStore。 另外还有一些辅助类的 API 目前都还处于 Experimental 状态。 Experimental 状态的 API 后续改变可能较大,甚至有可能删除,因此最好不要在生产环境使用。我们这里也暂时不讲这些 API,待它们稳定之后,我们再讲。
6. AsyncResource
6.1 抛出问题
讲完AsyncLocalStorage后,我们该讲讲AsyncResource了。
AsyncResource是一个类,继承于它可以创建一个异步资源类。
我们还是从一个问题开始,这样大家可能更容易理解。
举例如下:
js
const { AsyncLocalStorage } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
class DBQuery {
constructor() {
// 只有构造函数可以获取到 requestId
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
}
query(query, callback) {
// 无法获取到 requestId
console.log('无法获取到 requestId', asyncLocalStorage.getStore());
setTimeout(() => {
callback();
}, 500);
}
close() {
this.db = null;
}
}
for(let requestId = 0; requestId < 3; requestId++) {
const db = asyncLocalStorage.run(requestId, () => {
// 回调函数内部,成功获取到 requestId
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
// 由函数调用,变成了类实例创建
return new DBQuery();
});
// ...
// ...
// 一系列代码后
db.query('SELECT * FROM users', (err, results) => {
// 无法获取到 requestId
console.log('无法获取到 requestId', asyncLocalStorage.getStore());
});
}
假设DBQuery是一个数据库查询类,每次做数据库查询时,我们希望每处数据库请求相关的代码中都能拿到requestId,这样每条日志中都可以带上requestId,从而对每个请求的全过程进行追踪。
但是运行上面的代码你会发现,DBQuery中,除了构造函数中可以获取到requestId,其它地方都无法获取到。
我们的之前的例子一直在说run的回调函数中调用外部的函数时,外部函数是可以获取到store的。 但是这里我们不再是函数调用,而是改成了创建类的实例,这时仅仅类的构造函数中获取到了requestId,其他方法中都无法获取到。
那怎么这种情况怎么解决呢?
Node.js 的解决方案就是AsyncResource。
下面看看AsyncResource是如何解决该问题的。
js
const { AsyncLocalStorage, AsyncResource } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
class DBQuery extends AsyncResource {
constructor() {
super('DBQuery');
// 可以获取到 requestId
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
}
query(query, callback) {
setTimeout(() => {
this.runInAsyncScope(() => {
// 可以获取到 requestId
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
callback();
});
}, 500);
}
close() {
this.db = null;
}
}
for(let requestId = 0; requestId < 3; requestId++) {
const db = asyncLocalStorage.run(requestId, () => {
// 回调函数内部,成功获取到 requestId
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
return new DBQuery();
});
db.query('SELECT * FROM users', (err, results) => {
// 可以获取到 requestId
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
});
}
皆大欢喜,都拿到了requestId。
可以看到DBQuery继承于AsyncResource,这时run方法中的new DBQuery()返回的实例就是一个可追踪的异步资源了。DBQuery类可以通过调用runInAsyncScope方法,将需要获取store的代码包裹在其回调函数中。
这样我们就可以对每一个请求进行追踪。
相信看到这里大家对AsyncResource的作用有一个大概的了解了。
6.2 AsyncResource 详细讲解
上面讲 async_hooks方案时,我们讲过异步资源是和异步任务对应的,异步资源用于存储异步任务相关的数据,例如存储AsyncLocalStorage的store。它通常是由 Node.js 内部创建的。而AsyncResource的功能是让用户也可以创建一个异步资源,用AsyncResource创建异步资源时,async_hooks.createHook里的回调函数一样会被调用,和其他普通异步任务一样。
在上面的例子中,requestId的值只能在run函数创造的局部执行上下文中被访问,因此DBQuery的构造函数中可以拿到requestId的值。但是在run函数外面调用DBQuery的方法就无法获取到requestId,而让DBQuery继承AsyncResource后,new DBQuery()时,AsyncResource会将requestId的值存入自己的异步资源对象中,当调用runInAsyncScope时,其回调函数中调用getStore时,便会从此异步资源对象中获取到requestId的值。
6.2.1 创建对象
AsyncResource主要是用来被其他类继承,从而让该类具有了AsyncResouce的能力。 但是AsyncResouce也可以单独创建对象。 语法如下:
js
new AsyncResource(type[, options])
参数type: string类型。必填项。异步资源的名称,通常可以使用具体的类名,如果怕名字重复,也可以带上模块名称。
参数options: Object类型。可选项。 options包括如下属性: triggerAsyncId:number类型指定创建异步资源的任务块的asyncId。 requireManualDestroy:boolean类型。默认是false。如果是true,当异步资源被垃圾回收时不会出发销毁事件,即emitDestroy事件。如果是false,当异步资源被垃圾回收时,会自动触发emitDestroy事件。
6.2.2 asyncResource.runInAsyncScope(fn[, thisArg, ...args])
这是AsyncResource最核心的实例成员方法。 执行runInAsyncScope会同步的执行fn回调函数。 要想获取该异步资源的store,则必须在fn回调函数中执行getStore。
咱们上面已经举过例子了。这里再次贴出来:
js
const { AsyncLocalStorage, AsyncResource } = require('node:async_hooks');
const asyncLocalStorage = new AsyncLocalStorage();
class DBQuery extends AsyncResource {
constructor() {
super('DBQuery');
// 可以获取到 requestId
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
}
query(query, callback) {
setTimeout(() => {
this.runInAsyncScope(() => {
// 可以获取到 requestId
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
callback();
});
// 上面的匿名回调函数执行完毕后,才会打印 end,因为匿名回调函数是同步执行的
console.log('end');
}, 500);
}
close() {
this.db = null;
}
}
for(let requestId = 0; requestId < 3; requestId++) {
const db = asyncLocalStorage.run(requestId, () => {
// 回调函数内部,成功获取到 i
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
return new DBQuery();
});
db.query('SELECT * FROM users', (err, results) => {
// 可以获取到 requestId
console.log('可以获取到 requestId', asyncLocalStorage.getStore());
});
}
DBQuery继承于AsyncResource,因此具有了创建异步资源的能力,而它的异步资源由asyncLocalStorage.run的回调函数创建,因此里面存储了requestId的值,当调用runInAsyncScope方法时,其回调函数被同步执行,且可以通过getStore访问该异步资源。
6.2.3 AsyncResource.prototype.asyncId()
返回asyncId,number类型。 上面的Node.js 异步任务原理部分讲过,Node.js 会为每个异步任务分配一个asyncId。
6.2.4 AsyncResource.prototype.triggerAsyncId()
返回通过options传入的triggerAsyncId,number类型。 如果没有传入,则是创建AsyncResource对象的异步任务所对应的asyncId。
6.2.5 AsyncResource.prototype.emitDestroy()
返回调用它的AsyncResource实例。 调用该实例函数会触发调用async_hook.createHook方法指定的destroy回调函数。 Node.js 官方文档说多次调用会抛异常,但是使用 v20.10.0 版本实测,并不会抛异常,而且每次调用都会触发destroy回调。
6.2.6 bind方法将被删除
bind实例方法和bind静态方法都即将被删除。
7. 结束语
AsyncLocalStorage AsyncResource算是比较难理解的两个 Node.js 概念,必须结合 Node.js 的内部原理和实际案例来理解这两个 API。