非同步任務

有時候元件需要渲染僅能非同步取得的資料。這類資料可能會從伺服器、資料庫擷取,或通常從非同步 API 檢索或計算而來。

雖然 Lit 的響應式更新生命週期是批次且非同步的,但 Lit 樣板始終會同步渲染。樣板中使用的資料必須在渲染時可讀取。若要在 Lit 元件中渲染非同步資料,您必須等待資料準備就緒,將其儲存為可讀取的狀態,然後觸發新的渲染,以便同步使用該資料。通常也必須考慮在擷取資料時或資料擷取失敗時要渲染的內容。

@lit/task 套件提供 Task 響應式控制器,以協助管理此非同步資料工作流程。

Task 是一個控制器,它會接收非同步任務函式,並在其引數變更時手動或自動執行。Task 會儲存任務函式的結果,並在任務函式完成時更新主機元素,以便可以在渲染中使用該結果。

以下是使用 Task 透過 fetch() 呼叫 HTTP API 的範例。每當 productId 參數變更時,就會呼叫 API,且當正在擷取資料時,元件會渲染載入訊息。

Task 會處理正確管理非同步工作所需的許多事項

  • 當主機更新時收集任務引數
  • 當引數變更時執行任務函式
  • 追蹤任務狀態(初始、擱置中、完成或錯誤)
  • 儲存任務函式的最後完成值或錯誤
  • 當任務變更狀態時觸發主機更新
  • 處理競爭條件,確保只有最新的任務調用完成任務
  • 為目前任務狀態渲染正確的樣板
  • 允許使用 AbortController 中止任務

這會從您的程式碼中移除大部分正確使用非同步資料的樣板,並確保穩健處理競爭條件和其他邊緣案例。

非同步資料是無法立即取得,但可能會在未來某個時間取得的資料。例如,不是像字串或可同步使用的物件值,promise 會在未來提供值。

非同步資料通常從非同步 API 傳回,這可以採用幾種形式

  • Promise 或非同步函式,例如 fetch()
  • 接受回呼的函式
  • 發出事件的物件,例如 DOM 事件
  • 例如可觀察物件和訊號的函式庫

Task 控制器會處理 promise,因此無論您的非同步 API 的形狀為何,您都可以將其調整為 promise 以搭配 Task 使用。

Task 控制器的核心是「任務」本身的概念。

任務是一種非同步操作,會執行某些工作來產生資料,並以 Promise 傳回。任務可以處於幾種不同的狀態(初始、擱置中、完成和錯誤),而且可以採用參數。

任務是一種通用概念,可以代表任何非同步操作。它們最適用於具有請求/回應結構的情況,例如網路擷取、資料庫查詢或等待單一事件以回應某些動作。它們較不適用於自發或串流操作,例如開放式事件串流、串流資料庫回應等。

Task 是一種響應式控制器,因此它可以回應並觸發 Lit 響應式更新生命週期的更新。

您的元件通常會為每個邏輯任務設定一個 Task 物件。在您的類別上將任務安裝為欄位

作為類別欄位,可以輕鬆取得任務狀態和值

任務宣告最關鍵的部分是任務函式。這是執行實際工作的函式。

任務函式在 task 選項中提供。Task 控制器會自動使用引數呼叫任務函式,這些引數會透過個別的 args 回呼提供。會檢查引數是否有變更,而且只有在引數已變更時才會呼叫任務函式。

任務函式會將任務引數作為以第一個參數傳遞的陣列,並將選項引數作為第二個參數

任務函式的 args 陣列和 args 回呼的長度應相同。

taskargs 函式寫成箭頭函式,以便 this 參照指向主機元素。

任務可以處於四種狀態之一

  • INITIAL:尚未執行任務
  • PENDING:任務正在執行並等待新值
  • COMPLETE:任務已成功完成
  • ERROR:任務發生錯誤

Task 狀態可在 Task 控制器的 status 欄位取得,並由類似列舉的物件 TaskStatus 表示,該物件具有屬性 INITIALPENDINGCOMPLETEERROR

通常,Task 會從 INITIAL 進行到 PENDING,然後進行到 COMPLETEERROR 其中之一,然後如果重新執行任務,則會回到 PENDING。當任務變更狀態時,會觸發主機更新,以便主機元素可以處理新的任務狀態,並在需要時渲染。

了解任務可能處於的狀態很重要,但通常不需要直接存取它。

Task 控制器上有幾個與任務狀態相關的成員

  • status:任務的狀態。
  • value:任務的目前值(如果已完成)。
  • error:任務的目前錯誤(如果發生錯誤)。
  • render():根據目前狀態選擇要執行的回呼的方法。

用來渲染任務的最簡單和最常見的 API 是 task.render(),因為它會選擇要執行的正確程式碼並提供相關資料。

render() 採用具有每個任務狀態選用回呼的設定物件

  • initial()
  • pending()
  • complete(value)
  • error(err)

您可以在 Lit render() 方法內使用 task.render(),以根據任務狀態渲染樣板

依預設,只要引數變更,Tasks 就會執行。這由 autoRun 選項控制,預設值為 true

自動執行模式中,當主機更新時,任務會呼叫 args 函式,將引數與先前的引數進行比較,並在引數已變更時調用任務函式。沒有定義 args 的任務處於手動模式。

如果 autoRun 設定為 false,任務將處於手動模式。在手動模式中,您可以透過呼叫 .run() 方法來執行任務,這可能會從事件處理常式中呼叫

在手動模式中,您可以直接將新引數提供給 run()

如果未將引數提供給 run(),則會從 args 回呼收集引數。

可以在先前任務執行仍在擱置中時呼叫任務函式。在這些情況下,擱置中任務執行的結果將會被忽略,而且您應該嘗試取消任何未完成的工作或網路 I/O,以節省資源。

您可以使用傳遞至任務函式第二個參數的 signal 屬性中的 AbortSignal 來達成這個目的。當一個待處理的任務執行被新的執行取代時,傳遞給待處理執行的 AbortSignal 會被中止,以向任務執行發出訊號,取消任何待處理的工作。

AbortSignal 不會自動取消任何工作,它只是一個訊號。要取消某些工作,您必須自行檢查訊號,或者將訊號轉發到其他接受 AbortSignal 的 API,例如 fetch()addEventListener()

使用 AbortSignal 最簡單的方法是將其轉發到接受它的 API,例如 fetch()

如果訊號被中止,將訊號轉發到 fetch() 將會導致瀏覽器取消網路請求。

您也可以在任務函式中檢查訊號是否已中止。您應該在從非同步呼叫返回到任務函式後檢查訊號。throwIfAborted() 是一種方便的方法來執行此操作。

有時候您希望在一個任務完成時執行另一個任務。如果任務有不同的參數,使得鏈接的任務可以在不重新執行第一個任務的情況下執行,這會很有用。在這種情況下,它會將第一個任務用作快取。要執行此操作,您可以使用任務的值作為另一個任務的參數。

您也常常可以使用一個任務函式並等待中間結果。

TypeScript 中更精確的引數型別

連結至「TypeScript 中更精確的參數類型」

TypeScript 有時可能會過於寬鬆地推斷任務參數類型。這可以通過使用 as const 轉換參數陣列來修正。考慮以下具有兩個參數的任務。

如撰寫的,任務函式的參數列表的類型會被推斷為 Array<number | string>

但理想情況下,這應該被鍵入為元組 [number, string],因為參數的大小和位置是固定的。

args 的傳回值可以寫成 args: () => [this.myNumber, this.myText] as const,這將導致 task 函式的參數列表具有元組類型。