在现代的网络应用开发中,异步编程是一个基础且重要的概念。由于其能够在不阻塞主执行线程的情况下执行任务,异步操作成为构建高性能、能处理大量并发请求的应用程序的关键。然而,异步编程的管理尤其具有挑战性,特别是在需要在应用的不同部分间共享上下文时。例如,可能需要在异步操作间传递用户信息、跟踪信息或事务对象。此时,TC39 中的 Async Context for JavaScript 应运而生,它提供了一种在异步操作间存储和检索数据的方法。
漫漫长夜:寻找丢失的上下文
早期的 JavaScript 依赖于回调函数来处理异步任务,但这往往导致了复杂难懂的“回调地狱”。为了解决这一问题,JavaScript 引入了 Promises,它通过扁平化的结构来改善嵌套回调的问题,并最终通过 async/await 语法进一步增强了异步代码的可读性。这些工具使得开发者能够以类似同步代码的方式编写异步代码,但它们仍然无法完全处理事件循环中隐含上下文信息的丢失问题。
例如,下面代码中,在调用 implicit();异步函数时,函数内部 await结束后,控制权交出,执行了 finally块的代码,导致在后续代码中丢失了 shared变量的原始值
function program() {
const value = { key: 123 };
// Implicitly propagated via shared reference to an external variable.
// The value is only available only for the _synchronous execution_ of
// the try-finally code.
try {
shared = value;
implicit();
} finally {
shared = undefined;
}
}
let shared;
async function implicit() {
// The shared reference is still set to the correct value.
assert.equal(shared.key, 123);
await 1;
// After awaiting, the shared reference has been reset to `undefined`.
// We've lost access to our original value.
assert.throws(() => {
assert.equal(shared.key, 123);
});
}
program();
上下文的丢失。异步回调和事件循环使得跟踪和管理程序状态变得复杂,导致了诸如状态管理混乱、调试困难等问题。这不仅是 JavaScript 的痛点,也是整个异步编程世界的难题。
一丝曙光:AsyncContext 的革新
尽管 Promise 和 async/await 极大地改善了 JavaScript 的异步编程体验,它们并未提供一种明确的机制来跟踪和维护异步执行过程中的上下文。AsyncContext 填补了这一空白,提供了一种在异步执行流中保留上下文状态的标准方法。这意味着开发者可以更安全、更一致地处理异步代码中的状态变化,从而减少潜在的错误和不确定性。
AsyncContext 通过创建一个持久的上下文,使得在整个异步调用链中都可以访问和修改这个上下文。这样,无论代码执行到哪个异步阶段,相关的上下文信息都能被保留和追踪。它的优势在于:
- 上下文保持一致:无论多深的异步嵌套,上下文始终可访问。
- 调试友好:有助于跟踪异步流程,简化调试。
- 代码清晰:减少了因异步编程导致的复杂性。
该 API 目前处 于 ECMAScript 标准化过程的第 2 阶段,尽管距离发布为标准还有一段距离,但也展示了它在异步 JavaScript 编程中的潜力和实用性。
AsyncContext 的核心构件
AsyncContext API 的核心在于两个主要构件:AsyncContext.Variable和AsyncContext.Snapshot。Variable作为值的容器,与当前执行流相关联,并且可以在异步执行流中传播和恢复。Snapshot则允许在特定时间点捕获所有Variable的当前值,并在稍后以这些值作为当前值执行函数。
AsyncContext 的应用
在实际的应用场景中,AsyncContext.Variable允许在异步执行流中维护和传播上下文变量。例如在这段代码中, asyncVar 在执行 main 函数时被设置为字符串“top”。然后,可以使用 asyncVar.get() 在 main 函数中访问此值。请注意,通过 AsyncContext API , AsyncContext.Variable 实例被设计为在同步和异步操作中都能保持其值,例如使用 setTimeout 时。
const asyncVar = new AsyncContext.Variable();
// 设置当前值为'top'并执行`main`函数
asyncVar.run("top", main);
function main() {
setTimeout(() => {
console.log(asyncVar.get()); // 输出 'top'
// 在嵌套的AsyncContext.Variable运行中
asyncVar.run("A", () => {
console.log(asyncVar.get()); // 输出 'A'
});
}, randomTimeout());
}
另一方面,Snapshot类提供了一种方法,允许你用一种不易理解的方式捕获所有 Variable 的当前值,并在稍后的时间执行一个函数,在这个函数中保证这些值仍然和当前值一样(即快照和恢复功能)。
在这段代码中,snapshotDuringTop 生成了所有 AsyncContext.Variable 实例在其创建时的状态快照。随后,在 asyncVar 的 run 方法中,使用 snapshotDuringTop 的 run 方法执行了一个函数。即使此时 asyncVar 已设定为 C,在 snapshotDuringTop.run() 执行过程中, asyncVar 的值仍然是 top,因为这是在 snapshotDuringTop 创建时值的快照。
asyncVar.run('top', () => {
const snapshotDuringTop = new AsyncContext.Snapshot()
asyncVar.run('C', () => {
snapshotDuringTop.run(() => {
console.log(asyncVar.get()) // => 'top'
})
})
})
通过上述示例,我们可以看到 AsyncContext 的主要优势是提供了一种简单有效的方式来管理和传递异步操作中的上下文。然而,它也可能增加对底层实现的依赖,需要开发者对 JavaScript 的异步模型有更深入的理解。
破茧成蝶:终将成为标准
其实在 AsyncContext 提案的完善过程中,Node.js 已经实现了诸如 AsyncLocalStorage 和 AsyncResource 此类的用于跟踪异步操作间数据的线程本地存储机制,帮助服务端开发实现诸如 TraceId 等功能。但原生的 JavaScript 却仍然缺乏此类标准。
现在通过引入 AsyncContext API,JavaScript 开发者现在有了一种新的方法来更有效地管理和维护异步代码中的上下文信息。这不仅有助于减少异步编程中的错误和不可预测性,而且还为开发更复杂的异步 JavaScript 应用程序提供了更强大的工具。如追踪与日志记录,收集跨逻辑异步控制线程的性能信息,提供对应用程序性能的准确洞察等。随着 AsyncContext API 的进一步发展和完善,我们一定能够看到它在异步编程流行的当下大放异彩。
作者:ES2049
文章可随意转载,但请保留此 原文链接。
非常欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj@alibaba-inc.com 。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。