异步任务

有时组件需要渲染只有异步才能获取的数据。这些数据可能来自服务器、数据库,或者从异步 API 获取或计算。

虽然 Lit 的响应式更新生命周期是批量化的和异步的,但 Lit 模板始终同步渲染。模板中使用的数据必须在渲染时可读。要在 Lit 组件中渲染异步数据,您必须等待数据准备就绪,将其存储以便可以读取,然后触发新的渲染,该渲染可以同步使用该数据。通常需要考虑在获取数据时要渲染的内容,以及在数据获取失败时要渲染的内容。

@lit/task 包提供了 Task 响应式控制器来帮助管理这种异步数据工作流。

Task 是一个控制器,它接收一个异步任务函数,并在其参数发生变化时手动或自动运行它。Task 存储任务函数的结果,并在任务函数完成时更新宿主元素,以便结果可以在渲染中使用。

这是一个使用 Task 通过 fetch() 调用 HTTP API 的示例。每当 productId 参数更改时,都会调用该 API,并且当数据正在获取时,组件会渲染一个加载消息。

Task 处理了正确管理异步工作所需的一些事情

  • 在宿主更新时收集任务参数
  • 在参数发生变化时运行任务函数
  • 跟踪任务状态(初始、挂起、完成或错误)
  • 保存任务函数的最后完成值或错误
  • 在任务状态发生变化时触发宿主更新
  • 处理竞争条件,确保只有最新的任务调用才能完成任务
  • 为当前任务状态渲染正确的模板
  • 允许使用 AbortController 中止任务

这从您的代码中去除了大多数用于正确使用异步数据的样板代码,并确保了对竞争条件和其他边缘情况的稳健处理。

异步数据是无法立即获取,但在将来某个时间可能可以获取的数据。例如,与字符串或对象这样的同步可用值不同,Promise 在将来提供值。

异步数据通常来自异步 API,异步 API 可以有几种形式

  • Promise 或异步函数,如 fetch()
  • 接受回调的函数
  • 发出事件的对象,例如 DOM 事件
  • 像 Observable 和信号这样的库

Task 控制器处理 Promise,因此无论您的异步 API 是什么形状,您都可以将其调整为 Promise 以用于 Task。

Task 控制器核心的概念是“任务”本身。

任务是一个异步操作,它执行一些工作以生成数据并在 Promise 中返回数据。任务可以处于几个不同的状态(初始、挂起、完成和错误),并且可以接受参数。

任务是一个通用概念,可以代表任何异步操作。它们最适用于存在请求/响应结构的情况,例如网络获取、数据库查询或等待响应某些操作的单个事件。它们不太适用于自发或流式操作,例如开放式事件流、流式数据库响应等。

Task 是一个 响应式控制器,因此它可以响应 Lit 的响应式更新生命周期并触发更新。

通常,您每个组件需要执行的每个逻辑任务都会有一个 Task 对象。将任务安装为类中的字段

作为类字段,任务状态和值很容易获得

任务声明中最关键的部分是任务函数。这是执行实际工作的函数。

任务函数在 task 选项中给出。Task 控制器会自动使用参数调用任务函数,这些参数是使用单独的 args 回调提供的。参数会检查更改,只有在参数发生更改时才会调用任务函数。

任务函数将任务参数作为数组作为第一个参数传递,并将选项参数作为第二个参数传递

任务函数的 args 数组和 args 回调应具有相同的长度。

taskargs 函数编写为箭头函数,以便 this 引用指向宿主元素。

任务可以处于四种状态之一

  • INITIAL:任务尚未运行
  • PENDING:任务正在运行,等待新值
  • COMPLETE:任务已成功完成
  • ERROR:任务出错

任务状态可以在 Task 控制器的 status 字段中找到,它由 TaskStatus 枚举类对象表示,该对象具有属性 INITIALPENDINGCOMPLETEERROR

通常,任务将从 INITIAL 进入 PENDING,然后进入 COMPLETEERROR 之一,然后如果重新运行任务,则返回 PENDING。当任务状态发生变化时,它会触发宿主更新,以便宿主元素可以处理新的任务状态并根据需要进行渲染。

了解任务可能处于的状态很重要,但通常不需要直接访问它。

Task 控制器上有一些与任务状态相关的成员

  • status:任务的状态。
  • value:任务的当前值,如果它已完成。
  • error:任务的当前错误,如果它出错。
  • render():根据当前状态选择要运行的回调的方法。

渲染任务最简单、最常用的 API 是 task.render(),因为它选择正确的代码来运行并提供相关数据。

render() 接收一个配置对象,该对象包含每个任务状态的可选回调

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

您可以在 Lit render() 方法内部使用 task.render() 根据任务状态渲染模板

默认情况下,任务会在参数发生变化时运行。这是由 autoRun 选项控制的,该选项默认值为 true

自动运行模式下,当宿主更新时,任务会调用 args 函数,将 args 与之前的 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 函数的参数列表生成元组类型。