异步任务
有时组件需要渲染只有异步才能获取的数据。这些数据可能来自服务器、数据库,或者从异步 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,异步 API 可以有几种形式
- Promise 或异步函数,如
fetch()
- 接受回调的函数
- 发出事件的对象,例如 DOM 事件
- 像 Observable 和信号这样的库
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 控制器的 status
字段中找到,它由 TaskStatus
枚举类对象表示,该对象具有属性 INITIAL
、PENDING
、COMPLETE
和 ERROR
。
import {TaskStatus} from '@lit/task';
// ...
if (this.task.status === TaskStatus.ERROR) {
// ...
}
通常,任务将从 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>`,
})}
`;
}
运行任务
“运行任务”的永久链接默认情况下,任务会在参数发生变化时运行。这是由 autoRun
选项控制的,该选项默认值为 true
。
自动运行
“自动运行”的永久链接在自动运行模式下,当宿主更新时,任务会调用 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 以节省资源。
您可以使用任务函数第二个参数的 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
}
});
}