ECMAScript新提案:AsyncContext.Variable 和 AsyncContext.Snapshot
JavaScript 已成為編程中最通用和使用最廣泛的語言之一。無論您是在開發一個活潑的交互式網頁,還是為您的 Web 應用程序制作一個強大、可擴展的后端,JavaScript 都有您需要的工具和庫。但是,像所有語言一樣,它當然有其局限性和挑戰。其中一個挑戰是處理異步操作,這是編程的一個重要方面。
異步編程使 JavaScript 能夠以非阻塞的方式執行任務,這意味著它不必等待一個操作完成,然后再繼續下一個操作。它是一個功能強大的工具,但也可能導致棘手的問題,尤其是在管理異步調用之間的上下文時。
這個問題是新的異步上下文提案可以進行解決。本文提供了一個全面的指南,用于理解異步上下文提案,它對服務器端JavaScript的影響,以及它對JavaScript開發的未來可能意味著什么。
如果你熟悉異步 JavaScript 的歷史,我建議直接跳到什么是異步上下文 API?從這篇文章中獲得最大收益。
了解異步 JavaScript 代碼
在我們深入研究異步上下文提案之前,讓我們確保我們對異步代碼的含義達成共識。
簡單來說,異步編程是一種設計模式,它允許程序在等待操作完成的同時繼續執行其他任務。這種設計模式與同步編程形成鮮明對比,在同步編程中,程序按順序執行每個任務,并且僅在當前任務完成后才繼續執行下一個任務。
JavaScript是一種單線程語言,這意味著它一次只能做一件事。但是,它需要處理許多操作,如網絡請求、計時器等,這些操作不能完全適應單線程任務隊列。這就是事件循環的用武之地。
事件循環
事件循環是 JavaScript 環境中支持異步 JavaScript 的重要組成部分。它不斷循環遍歷調用堆棧并檢查要執行的任務。
假設一個任務還沒有準備好執行,例如當它正在等待來自服務器的數據時——事件循環可以移動到下一個任務,并在準備好后返回到等待任務。這樣,JavaScript 可以隨著時間的推移處理多個任務,而不會停止或阻止程序的其余部分。
JavaScript 提供了幾種編寫異步代碼的技術。早期版本的 JavaScript 依賴于回調,其中一個函數將傳遞到另一個函數并在以后運行。然而,這種方法導致了臭名昭著的“回調地獄”,這是一種深度嵌套回調的情況,使代碼難以閱讀和維護。
JavaScript的Promise和 async/await
為了解決這個問題,JavaScript 引入了 Promise,它表示異步操作的最終結果(成功或失敗)及其結果值。Promise有助于扁平化嵌套回調,并使異步代碼更易于使用。
他們還支持使用 async/await 語法,進一步提高了異步 JavaScript 代碼的可讀性。通過 async/await,開發人員可以編寫讀起來與同步代碼非常相似的異步代碼,從而更輕松地理解數據流和事件。
然而,雖然這些工具使異步JavaScript更易于管理,但它們仍然存在一些問題。主要問題之一是通過事件循環傳遞代碼時隱式上下文丟失。讓我們在下一節中深入研究這一挑戰。
異步上下文的挑戰
正如我們所提到的,JavaScript的事件循環 - 結合Promise和async/await - 允許開發人員編寫異步代碼,比嵌套回調更容易管理和理解。但是,這些工具存在自己的困難:通過事件循環傳遞代碼時,來自調用站點的某些隱式信息會丟失。
這個“隱含信息”是什么?在同步代碼執行中,值在執行的生命周期內始終可用。它們可以顯式傳遞,如函數參數或封閉變量(在外部作用域中定義,但由內部函數引用),也可以隱式傳遞,例如存儲在多個作用域可訪問的變量中的值。
但是,當涉及到異步執行時,情況會發生變化。讓我們用一個例子來說明這一點。假設您有一個在不同函數作用域之間共享的變量,并且其值在異步函數執行期間發生變化。
在函數執行開始時,共享變量可能具有一定的值。但是,當函數完成時(例如, await 在操作之后),該共享變量的值可能已更改,從而導致潛在的意外結果。
這就是在異步 JavaScript 中丟失隱式調用站點信息的問題出現的地方。無論您使用的是Promise鏈還是async/await,事件循環的行為都保持不變。
但是,當事件循環執行異步代碼時,調用堆棧(跟蹤函數調用以及函數調用后返回位置的機制)會被修改。這意味著在原始調用站點上隱式提供的信息可能會在異步函數完成時丟失。
async/await 語法的引入雖然總體上是一個積極的變化,但使這個問題進一步復雜化。由于 async/await 使異步代碼看起來與同步代碼非常相似,因此更難看到調用堆棧和上下文被修改的位置。這可能會導致錯誤和不可預測的行為,以及難以追蹤的意外錯誤。
認識到這個問題,TC39委員會成員Chengzhong Wu和Justin Ridgewell一直在尋求一種在異步操作中保留上下文的方法。他們的解決方案是異步上下文API。
什么是異步上下文 API?
異步上下文 API 是一種強大的新機制,用于處理異步上下文提案中提出的異步 JavaScript 代碼中的上下文。截至 2023 年 8 月,該提案目前處于 ECMAScript 標準化流程的第 2 階段,這意味著它已被批準為提案草案,目前正在積極開發中。盡管該提案仍可能發生變化,但它被認為是包含在下一版 JavaScript 中的可行候選者。
異步上下文 API 將允許 JavaScript 通過事件循環在轉換中捕獲和使用隱式調用站點信息。它允許開發人員編寫與同步代碼非常相似的異步代碼,即使在需要隱式信息的情況下也是如此。
此 API 圍繞兩個基本構造展開: AsyncContext.Variable 和 AsyncContext.Snapshot 。這些構造允許開發人員在異步代碼塊中創建、操作和檢索上下文變量,從而提供通過異步代碼(如 Promise 延續或異步回調)傳播值的機制。
AsyncContext.Variable 類
該 AsyncContext.Variable 類允許您創建一個可以在異步上下文中設置和訪問的新變量。該方法 run 在函數執行期間為變量設置值,而 get 該方法檢索變量的當前值。
AsyncContext.Snapshot 類
另一方面,該 AsyncContext.Snapshot 類使您能夠捕獲給定 AsyncContext.Variable 時間所有實例的當前值。這就像拍攝所有上下文變量狀態的“快照”,然后可以使用這些快照在以后使用這些捕獲的值執行函數。
這些新結構有望大大簡化異步JavaScript中的上下文管理。借助異步上下文 API,開發人員可以保持一致對所需隱式信息的訪問,無論他們執行多少異步操作。
使用 AsyncContext
讓我們看一下異步上下文提案 AsyncContext 中的命名空間聲明:
namespace AsyncContext {
class Variable<T> {
constructor(options: AsyncVariableOptions<T>);
get name(): string;
run<R>(value: T, fn: (...args: any[])=> R, ...args: any[]): R;
get(): T | undefined;
}
interface AsyncVariableOptions<T> {
name?: string;
defaultValue?: T;
}
class Snapshot {
constructor();
run<R>(fn: (...args: any[]) => R, ...args: any[]): R;
}
}
AsyncContext 命名空間引入了幾個基本類和一個接口:
- Variable :此類表示可以保存值的上下文變量;泛型類型
<T>
指示變量可以保存任何數據類型。它還有一個 run() 設置變量值并執行函數的方法,其 get() 方法返回變量的當前值 - AsyncVariableOptions :這是一個接口,用于定義可以傳遞給構造函數的 Variable 可能選項。它可以包含變量的名稱和默認值
- Snapshot :此類捕獲特定 AsyncContext.Variables 時刻的所有狀態。它有一個 run() 方法,該方法使用拍攝快照時的變量值執行函數
理解 AsyncContext.Variable
讓我們更深入地了解工作原理 AsyncContext.Variable 。假設您有一個 asyncVar 的實例 AsyncContext.Variable 。若要在函數執行期間設置 其 asyncVar 值,請使用 run 該方法。此方法將以下內容作為其前兩個參數:
- 要設置的值 asyncVar
- 其執行 asyncVar 應保存該值的函數
下面是一個示例:
const asyncVar = new AsyncContext.Variable()
asyncVar.run('top', main)
在此代碼中, asyncVar 在 main 函數執行期間設置為字符串“top”。然后可以使用 在 main asyncVar.get() 函數中訪問此值。請注意,通過 AsyncContext API, AsyncContext.Variable 實例旨在跨同步和異步操作(例如使用 setTimeout 。
有趣的是, AsyncContext.Variable 運行可以嵌套。這意味著您可以在已運行的函數 asyncVar 中調用該方法, run 并設定了 的值 asyncVar 為 。在這種情況下,在內部函數執行期間采用新值, asyncVar 然后在內部函數完成后恢復為以前的值。
理解 AsyncContext.Snapshot
該 AsyncContext.Snapshot 類用于捕獲特定時間點所有 AsyncContext.Variable 實例的狀態。這是通過調用構造函數來完成的 Snapshot 。快照稍后可用于運行函數,就好像捕獲的值仍然是其各自變量的當前值一樣。這是通過 Snapshot 實例的方法完成 run 的。
讓我們看一個例子:
asyncVar.run('top', () => {
const snapshotDuringTop = new AsyncContext.Snapshot()
asyncVar.run('C', () => {
snapshotDuringTop.run(() => {
console.log(asyncVar.get()) // => 'top'
})
})
})
在此代碼中, 生成 snapshotDuringTop 創建時所有 AsyncContext.Variable 實例狀態的快照。稍后,在asyncVar的方法中 run ,使用 run.snapshotDuringTop的asyncVar方法。
即使此時設置為“C”,在 asyncVar snapshotDuringTop.run() 執行中,的值 asyncVar 仍然是“top”,因為這是創建時 snapshotDuringTop 的值。
異步上下文 API 的影響
提議的Async Context API對于JavaScript開發人員具有巨大的潛力,特別是那些使用Node.js等服務器端JavaScript環境的開發人員。此 API 將使開發人員能夠更有效地處理異步代碼中的上下文,提供在執行的特定點設置、檢索和“凍結”上下文的函數。
這可能會使開發人員擺脫在異步操作中管理上下文的復雜性,并大大增強可跟蹤性和調試,從而可以更好地專注于編寫有效和高效的代碼。
更實際地說,如果實現,異步上下文可以顯著增強各種用例,例如:
- 使用與異步調用堆棧相關的信息批注日志
- 跨邏輯異步控制線程收集性能信息
- 提供對應用程序性能的準確洞察
它還可以通過在整個請求處理過程中維護上下文來改進 Web API 的功能,例如優先級任務計劃,甚至跨多個異步操作。
此外,異步上下文 API 可以幫助改進錯誤處理,使開發人員能夠更準確地將異常跟蹤到其原始上下文,從而縮短解決時間。它還可以通過跨異步邊界管理和維護上下文來提供更有效的并發控制。
在更大的范圍內,Async Context提案代表了JavaScript持續發展的重要進步,特別是對于異步編程。通過解決異步編程帶來的一些獨特挑戰,它可以提供一個強大、靈活的解決方案。然而,該提案仍處于早期階段,其最終影響將取決于其在 JavaScript 社區中的最終實現和采用。
寫在最后
如果實現,異步上下文 API 將為開發人員提供更強大的工具來管理異步代碼中的上下文。在異步環境中傳播上下文的能力可能是變革性的,從而帶來更準確的跟蹤、調試和性能監視。
JavaScript 的未來可能會繼續強調異步操作的有效處理,而像 Async Context 這樣的提案代表了邁向未來的必要步驟。