非同期タスク
コンポーネントでは、非同期的にしか利用できないデータをレンダリングする必要がある場合があります。そのようなデータは、サーバー、データベース、または一般的な非同期 API から取得または計算される場合があります。
Lit のリアクティブ更新ライフサイクルはバッチ処理され非同期ですが、Lit テンプレートは常に同期的にレンダリングされます。テンプレートで使用されるデータは、レンダリング時に読み取り可能である必要があります。Lit コンポーネントで非同期データをレンダリングするには、データの準備が整うまで待機し、読み取り可能になるように保存してから、データを同期的に使用できる新しいレンダリングをトリガーする必要があります。データの取得中またはデータ取得が失敗した場合に何をレンダリングするかについても、多くの場合考慮する必要があります。
@lit/task
パッケージは、この非同期データワークフローの管理に役立つ Task
リアクティブコントローラーを提供します。
Task
は、非同期タスク関数を取得し、引数が変更されたときに手動または自動的に実行するコントローラーです。Task はタスク関数の結果を保存し、タスク関数が完了するとホスト要素を更新して、結果をレンダリングで使用できるようにします。
これは、fetch()
を介して HTTP API を呼び出すために Task
を使用する例です。API は productId
パラメーターが変更されるたびに呼び出され、データの取得中はコンポーネントが読み込みメッセージをレンダリングします。
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 から返されます。これはいくつかの形式で提供されます。
fetch()
のような Promise や非同期関数- コールバックを受け取る関数
- DOM イベントなど、イベントを発行するオブジェクト
- Observable やシグナルなどのライブラリ
Task コントローラーは Promise を処理するため、非同期 API の形状に関係なく、Task で使用するために Promise に適応できます。
タスクとは?
“タスクとは?”へのパーマリンクTask コントローラーの中核にあるのは、「タスク」そのものの概念です。
タスクとは、データを生成して Promise で返す作業を行う非同期操作です。タスクはいくつかの異なる状態(初期、保留中、完了、およびエラー)になり、パラメーターを取ることができます。
タスクは一般的な概念であり、任意の非同期操作を表すことができます。それらは、ネットワークフェッチ、データベースクエリ、または何らかのアクションに応答して単一のイベントを待機するなど、要求/応答構造がある場合に最も有効です。それらは、イベントの無限のストリーム、ストリーミングデータベース応答など、自発的またはストリーミング操作にはあまり適用されません。
インストール
“インストール”へのパーマリンクnpm install @lit/task
使用方法
“使用方法”へのパーマリンクTask
は リアクティブコントローラー なので、Lit のリアクティブ更新ライフサイクルに応答し、それをトリガーできます。
一般的に、コンポーネントが実行する必要がある各論理タスクに対して 1 つの 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
コールバックで提供される引数を使用して、タスク関数を自動的に呼び出します。引数の変更がチェックされ、引数が変更された場合にのみタスク関数が呼び出されます。
タスク関数は、最初の引数として渡される配列としてタスク引数を受け取り、2 番目の引数としてオプション引数を受け取ります。
new Task(this, {
task: async ([arg1, arg2], {signal}) => {
// do async work here
},
args: () => [this.field1, this.field2]
})
タスク関数の args 配列と args コールバックの長さは同じである必要があります。
this
参照がホスト要素を指すように、task
関数と args
関数をアロー関数として記述します。
タスクの状態
“タスクの状態”へのパーマリンクタスクは 4 つの状態のいずれかになります。
INITIAL
: タスクは実行されていません。PENDING
: タスクは実行中で、新しい値を待っています。COMPLETE
: タスクは正常に完了しました。ERROR
: タスクでエラーが発生しました。
Task の状態は、Task コントローラーの status
フィールドで利用でき、INITIAL
、PENDING
、COMPLETE
、および ERROR
のプロパティを持つ TaskStatus
enum のようなオブジェクトによって表されます。
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()
は、各タスク状態のオプションのコールバックを含む config オブジェクトを受け取ります。
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>`,
})}
`;
}
タスクの実行
“タスクの実行”へのパーマリンクデフォルトでは、タスクは引数が変更されるたびに実行されます。これは、デフォルトで true
に設定されている autoRun
オプションによって制御されます。
自動実行
“自動実行”へのパーマリンク自動実行モードでは、ホストが更新されたときにタスクは args
関数を呼び出し、args を前の 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 をキャンセルする必要があります。
タスク関数の第2引数の`signal`プロパティで渡されるAbortSignal
を使用して処理できます。保留中のタスクの実行が新しい実行によって置き換えられると、保留中の実行に渡されたAbortSignal
が中断され、タスクの実行に保留中の作業をキャンセルするよう信号が送信されます。
AbortSignal
は、作業を自動的にキャンセルするわけではありません。単なる信号です。作業をキャンセルするには、信号を確認して自分で行うか、fetch()
やaddEventListener()
など、AbortSignal
を受け入れる別のAPIに信号を転送する必要があります。
AbortSignal
を使用する最も簡単な方法は、fetch()
など、それを受け入れるAPIに転送することです。
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;
},
});
別のタスクが完了したときに1つのタスクを実行したい場合があります。タスクに異なる引数がある場合、チェーンされたタスクは最初のタスクが再度実行されなくても実行できるため、これは便利です。この場合、最初のタスクをキャッシュとして使用します。これを行うには、タスクの値を別のタスクの引数として使用できます。
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],
});
}
1つのタスク関数を使用して、中間結果をawaitすることもできます。
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 でより正確な引数の型
Permalink to “More accurate argument types in TypeScript”タスクの引数の型は、TypeScriptによって緩すぎる場合もあります。これは、as const
を使用して引数配列をキャストすることで修正できます。次のタスク(2つの引数を持つ)を考えてみましょう。
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
関数のargsリストにタプル型が生成されます。
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
}
});
}