异步任务
有时组件需要渲染只有异步才能获取的数据。这些数据可能来自服务器、数据库,或者从异步 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/taskTask 是一个 响应式控制器,因此它可以响应 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 } });}