非同步任務
有時候元件需要渲染僅能非同步取得的資料。這類資料可能會從伺服器、資料庫擷取,或通常從非同步 API 檢索或計算而來。
雖然 Lit 的響應式更新生命週期是批次且非同步的,但 Lit 樣板始終會同步渲染。樣板中使用的資料必須在渲染時可讀取。若要在 Lit 元件中渲染非同步資料,您必須等待資料準備就緒,將其儲存為可讀取的狀態,然後觸發新的渲染,以便同步使用該資料。通常也必須考慮在擷取資料時或資料擷取失敗時要渲染的內容。
@lit/task
套件提供 Task
響應式控制器,以協助管理此非同步資料工作流程。
Task
是一個控制器,它會接收非同步任務函式,並在其引數變更時手動或自動執行。Task 會儲存任務函式的結果,並在任務函式完成時更新主機元素,以便可以在渲染中使用該結果。
以下是使用 Task
透過 fetch()
呼叫 HTTP API 的範例。每當 productId
參數變更時,就會呼叫 API,且當正在擷取資料時,元件會渲染載入訊息。
import {Task} from '@lit/task';
class MyElement extends LitElement {
@property() productId?: string;
private _productTask = new Task(this, {
task: async ([productId], {signal}) => {
const response = await fetch(`http://example.com/product/${productId}`, {signal});
if (!response.ok) { throw new Error(response.status); }
return response.json() as Product;
},
args: () => [this.productId]
});
render() {
return this._productTask.render({
pending: () => html`<p>Loading product...</p>`,
complete: (product) => html`
<h1>${product.name}</h1>
<p>${product.price}</p>
`,
error: (e) => html`<p>Error: ${e}</p>`
});
}
}
import {Task} from '@lit/task';
class MyElement extends LitElement {
static properties = {
productId: {},
};
_productTask = new Task(this, {
task: async ([productId], {signal}) => {
const response = await fetch(`http://example.com/product/${productId}`, {signal});
if (!response.ok) { throw new Error(response.status); }
return response.json();
},
args: () => [this.productId]
});
render() {
return this._productTask.render({
pending: () => html`<p>Loading product...</p>`,
complete: (product) => html`
<h1>${product.name}</h1>
<p>${product.price}</p>
`,
error: (e) => html`<p>Error: ${e}</p>`
});
}
}
Task 會處理正確管理非同步工作所需的許多事項
- 當主機更新時收集任務引數
- 當引數變更時執行任務函式
- 追蹤任務狀態(初始、擱置中、完成或錯誤)
- 儲存任務函式的最後完成值或錯誤
- 當任務變更狀態時觸發主機更新
- 處理競爭條件,確保只有最新的任務調用完成任務
- 為目前任務狀態渲染正確的樣板
- 允許使用
AbortController
中止任務
這會從您的程式碼中移除大部分正確使用非同步資料的樣板,並確保穩健處理競爭條件和其他邊緣案例。
什麼是非同步資料?
「什麼是非同步資料?」的永久連結非同步資料是無法立即取得,但可能會在未來某個時間取得的資料。例如,不是像字串或可同步使用的物件值,promise 會在未來提供值。
非同步資料通常從非同步 API 傳回,這可以採用幾種形式
- Promise 或非同步函式,例如
fetch()
- 接受回呼的函式
- 發出事件的物件,例如 DOM 事件
- 例如可觀察物件和訊號的函式庫
Task 控制器會處理 promise,因此無論您的非同步 API 的形狀為何,您都可以將其調整為 promise 以搭配 Task 使用。
什麼是任務?
「什麼是任務?」的永久連結Task 控制器的核心是「任務」本身的概念。
任務是一種非同步操作,會執行某些工作來產生資料,並以 Promise 傳回。任務可以處於幾種不同的狀態(初始、擱置中、完成和錯誤),而且可以採用參數。
任務是一種通用概念,可以代表任何非同步操作。它們最適用於具有請求/回應結構的情況,例如網路擷取、資料庫查詢或等待單一事件以回應某些動作。它們較不適用於自發或串流操作,例如開放式事件串流、串流資料庫回應等。
npm install @lit/task
Task
是一種響應式控制器,因此它可以回應並觸發 Lit 響應式更新生命週期的更新。
您的元件通常會為每個邏輯任務設定一個 Task 物件。在您的類別上將任務安裝為欄位
class MyElement extends LitElement {
private _myTask = new Task(this, {/*...*/});
}
class MyElement extends LitElement {
_myTask = new Task(this, {/*...*/});
}
作為類別欄位,可以輕鬆取得任務狀態和值
this._task.status;
this._task.value;
任務函式
「任務函式」的永久連結任務宣告最關鍵的部分是任務函式。這是執行實際工作的函式。
任務函式在 task
選項中提供。Task 控制器會自動使用引數呼叫任務函式,這些引數會透過個別的 args
回呼提供。會檢查引數是否有變更,而且只有在引數已變更時才會呼叫任務函式。
任務函式會將任務引數作為以第一個參數傳遞的陣列,並將選項引數作為第二個參數
new Task(this, {
task: async ([arg1, arg2], {signal}) => {
// do async work here
},
args: () => [this.field1, this.field2]
})
任務函式的 args 陣列和 args 回呼的長度應相同。
將 task
和 args
函式寫成箭頭函式,以便 this
參照指向主機元素。
任務狀態
「任務狀態」的永久連結任務可以處於四種狀態之一
INITIAL
:尚未執行任務PENDING
:任務正在執行並等待新值COMPLETE
:任務已成功完成ERROR
:任務發生錯誤
Task 狀態可在 Task 控制器的 status
欄位取得,並由類似列舉的物件 TaskStatus
表示,該物件具有屬性 INITIAL
、PENDING
、COMPLETE
和 ERROR
。
import {TaskStatus} from '@lit/task';
// ...
if (this.task.status === TaskStatus.ERROR) {
// ...
}
通常,Task 會從 INITIAL
進行到 PENDING
,然後進行到 COMPLETE
或 ERROR
其中之一,然後如果重新執行任務,則會回到 PENDING
。當任務變更狀態時,會觸發主機更新,以便主機元素可以處理新的任務狀態,並在需要時渲染。
了解任務可能處於的狀態很重要,但通常不需要直接存取它。
Task 控制器上有幾個與任務狀態相關的成員
status
:任務的狀態。value
:任務的目前值(如果已完成)。error
:任務的目前錯誤(如果發生錯誤)。render()
:根據目前狀態選擇要執行的回呼的方法。
渲染任務
「渲染任務」的永久連結用來渲染任務的最簡單和最常見的 API 是 task.render()
,因為它會選擇要執行的正確程式碼並提供相關資料。
render()
採用具有每個任務狀態選用回呼的設定物件
initial()
pending()
complete(value)
error(err)
您可以在 Lit render()
方法內使用 task.render()
,以根據任務狀態渲染樣板
render() {
return html`
${this._myTask.render({
initial: () => html`<p>Waiting to start task</p>`,
pending: () => html`<p>Running task...</p>`,
complete: (value) => html`<p>The task completed with: ${value}</p>`,
error: (error) => html`<p>Oops, something went wrong: ${error}</p>`,
})}
`;
}
執行任務
「執行任務」的永久連結依預設,只要引數變更,Tasks 就會執行。這由 autoRun
選項控制,預設值為 true
。
自動執行
「自動執行」的永久連結在自動執行模式中,當主機更新時,任務會呼叫 args
函式,將引數與先前的引數進行比較,並在引數已變更時調用任務函式。沒有定義 args
的任務處於手動模式。
手動模式
「手動模式」的永久連結如果 autoRun
設定為 false,任務將處於手動模式。在手動模式中,您可以透過呼叫 .run()
方法來執行任務,這可能會從事件處理常式中呼叫
class MyElement extends LitElement {
private _getDataTask = new Task(
this,
{
task: async () => {
const response = await fetch(`example.com/data/`);
return response.json();
},
args: () => []
}
);
render() {
return html`
<button @click=${this._onClick}>Get Data</button>
`;
}
private _onClick() {
this._getDataTask.run();
}
}
class MyElement extends LitElement {
_getDataTask = new Task(
this,
{
task: async () => {
const response = await fetch(`example.com/data/`);
return response.json();
},
args: () => []
}
);
render() {
return html`
<button @click=${this._onClick}>Get Data</button>
`;
}
_onClick() {
this._getDataTask.run();
}
}
在手動模式中,您可以直接將新引數提供給 run()
this._task.run(['arg1', 'arg2']);
如果未將引數提供給 run()
,則會從 args
回呼收集引數。
中止任務
「中止任務」的永久連結可以在先前任務執行仍在擱置中時呼叫任務函式。在這些情況下,擱置中任務執行的結果將會被忽略,而且您應該嘗試取消任何未完成的工作或網路 I/O,以節省資源。
您可以使用傳遞至任務函式第二個參數的 signal
屬性中的 AbortSignal
來達成這個目的。當一個待處理的任務執行被新的執行取代時,傳遞給待處理執行的 AbortSignal
會被中止,以向任務執行發出訊號,取消任何待處理的工作。
AbortSignal
不會自動取消任何工作,它只是一個訊號。要取消某些工作,您必須自行檢查訊號,或者將訊號轉發到其他接受 AbortSignal
的 API,例如 fetch()
或 addEventListener()
。
使用 AbortSignal
最簡單的方法是將其轉發到接受它的 API,例如 fetch()
。
private _task = new Task(this, {
task: async (args, {signal}) => {
const response = await fetch(someUrl, {signal});
// ...
},
});
_task = new Task(this, {
task: async (args, {signal}) => {
const response = await fetch(someUrl, {signal});
// ...
},
});
如果訊號被中止,將訊號轉發到 fetch()
將會導致瀏覽器取消網路請求。
您也可以在任務函式中檢查訊號是否已中止。您應該在從非同步呼叫返回到任務函式後檢查訊號。throwIfAborted()
是一種方便的方法來執行此操作。
private _task = new Task(this, {
task: async ([arg1], {signal}) => {
const firstResult = await doSomeWork(arg1);
signal.throwIfAborted();
const secondResult = await doMoreWork(firstResult);
signal.throwIfAborted();
return secondResult;
},
});
_task = new Task(this, {
task: async ([arg1], {signal}) => {
const firstResult = await doSomeWork(arg1);
signal.throwIfAborted();
const secondResult = await doMoreWork(firstResult);
signal.throwIfAborted();
return secondResult;
},
});
任務串連
連結至「任務鏈」有時候您希望在一個任務完成時執行另一個任務。如果任務有不同的參數,使得鏈接的任務可以在不重新執行第一個任務的情況下執行,這會很有用。在這種情況下,它會將第一個任務用作快取。要執行此操作,您可以使用任務的值作為另一個任務的參數。
class MyElement extends LitElement {
private _getDataTask = new Task(this, {
task: ([dataId]) => getData(dataId),
args: () => [this.dataId],
});
private _processDataTask = new Task(this, {
task: ([data, param]) => processData(data, param),
args: () => [this._getDataTask.value, this.param],
});
}
class MyElement extends LitElement {
_getDataTask = new Task(this, {
task: ([dataId]) => getData(dataId),
args: () => [this.dataId],
});
_processDataTask = new Task(this, {
task: ([data, param]) => processData(data, param),
args: () => [this._getDataTask.value, this.param],
});
}
您也常常可以使用一個任務函式並等待中間結果。
class MyElement extends LitElement {
private _getDataTask = new Task(this, {
task: ([dataId, param]) => {
const data = await getData(dataId);
return processData(data, param);
},
args: () => [this.dataId, this.param],
});
}
class MyElement extends LitElement {
_getDataTask = new Task(this, {
task: ([dataId, param]) => {
const data = await getData(dataId);
return processData(data, param);
},
args: () => [this.dataId, this.param],
});
}
TypeScript 中更精確的引數型別
連結至「TypeScript 中更精確的參數類型」TypeScript 有時可能會過於寬鬆地推斷任務參數類型。這可以通過使用 as const
轉換參數陣列來修正。考慮以下具有兩個參數的任務。
class MyElement extends LitElement {
@property() myNumber = 10;
@property() myText = "Hello world";
_myTask = new Task(this, {
args: () => [this.myNumber, this.myText],
task: ([number, text]) => {
// implementation omitted
}
});
}
如撰寫的,任務函式的參數列表的類型會被推斷為 Array<number | string>
。
但理想情況下,這應該被鍵入為元組 [number, string]
,因為參數的大小和位置是固定的。
args
的傳回值可以寫成 args: () => [this.myNumber, this.myText] as const
,這將導致 task
函式的參數列表具有元組類型。
class MyElement extends LitElement {
@property() myNumber = 10;
@property() myText = "Hello world";
_myTask = new Task(this, {
args: () => [this.myNumber, this.myText] as const,
task: ([number, text]) => {
// implementation omitted
}
});
}