自定义指令

指令是函数,可以通过自定义模板表达式的渲染方式来扩展 Lit。 指令非常有用且强大,因为它们可以是有状态的,可以访问 DOM,可以在模板断开连接和重新连接时收到通知,并且可以在渲染调用之外独立更新表达式。

在您的模板中使用指令就像在模板表达式中调用函数一样简单

Lit 附带了一些 内置指令,例如 repeat()cache()。 用户也可以编写自己的自定义指令。

指令有两种类型

  • 简单函数
  • 基于类的指令

简单函数返回一个值进行渲染。 它可以接受任意数量的参数,或者根本不接受参数。

基于类的指令允许您做简单函数无法做到的事情。 使用基于类的指令来

  • 直接访问渲染的 DOM(例如,添加、删除或重新排序渲染的 DOM 节点)。
  • 在渲染之间保留状态。
  • 在渲染调用之外异步更新 DOM。
  • 在指令与 DOM 断开连接时清理资源

本页的其余部分描述了基于类的指令。

要创建基于类的指令

  • 将指令实现为扩展 Directive 类的类。
  • 将您的类传递给 directive() 工厂以创建一个指令函数,该函数可以在 Lit 模板表达式中使用。

当此模板被评估时,指令函数hello())将返回一个 DirectiveResult 对象,该对象指示 Lit 创建或更新指令HelloDirective)的实例。 然后 Lit 调用指令实例上的方法来运行其更新逻辑。

某些指令需要在正常的更新周期之外异步更新 DOM。 要创建一个异步指令,请扩展 AsyncDirective 基类而不是 Directive。 有关详细信息,请参阅 异步指令

指令类有一些内置的生命周期方法

  • 类构造函数,用于一次性初始化。
  • render(),用于声明式渲染。
  • update(),用于命令式 DOM 访问。

您必须为所有指令实现 render() 回调。 实现 update() 是可选的。 update() 的默认实现调用并返回 render() 的值。

异步指令可以在正常的更新周期之外更新 DOM,它们使用一些额外的生命周期回调。 有关详细信息,请参阅 异步指令

当 Lit 首次在表达式中遇到 DirectiveResult 时,它将构造一个对应指令类的实例(导致指令的构造函数和任何类字段初始化器运行)

只要在每次渲染时在同一个表达式中使用同一个指令函数,就会重用之前的实例,因此实例的状态在渲染之间保留。

构造函数接收一个 PartInfo 对象,它提供有关使用指令的表达式的元数据。 这对于在指令旨在仅用于特定类型表达式的情况下提供错误检查很有用(请参阅 将指令限制为一种表达式类型)。

render() 方法应该返回渲染到 DOM 中的值。 它可以返回任何可渲染的值,包括另一个 DirectiveResult

除了引用指令实例上的状态外,render() 方法还可以接受传递给指令函数的任意参数

render() 方法定义的参数确定指令函数的签名

在更高级的用例中,您的指令可能需要访问底层 DOM 并命令式地从中读取或修改它。 您可以通过覆盖 update() 回调来实现这一点。

update() 回调接收两个参数

  • 一个 Part 对象,它提供了一个直接管理与表达式关联的 DOM 的 API。
  • 包含 render() 参数的数组。

您的 update() 方法应该返回 Lit 可以渲染的东西,或者如果不需要重新渲染,则返回特殊值 noChangeupdate() 回调非常灵活,但典型用途包括

  • 从 DOM 中读取数据,并使用它生成要渲染的值。
  • 使用 Part 对象上的 elementparentNode 引用命令式地更新 DOM。 在这种情况下,update() 通常返回 noChange,表示 Lit 不需要采取任何进一步的操作来渲染指令。

每个表达式位置都有其自己的特定 Part 对象

  • ChildPart 用于 HTML 子位置中的表达式。
  • AttributePart 用于 HTML 属性值位置中的表达式。
  • BooleanAttributePart 用于布尔属性值(名称以 ? 为前缀)位置中的表达式。
  • EventPart 用于事件侦听器位置(名称以 @ 为前缀)中的表达式。
  • PropertyPart 用于属性值位置(名称以 . 为前缀)中的表达式。
  • ElementPart 用于元素标签上的表达式。

除了 PartInfo 中包含的特定于部分的元数据外,所有 Part 类型都提供对与表达式关联的 DOM element(或在 ChildPart 的情况下为 parentNode)的访问权限,这些元素可以在 update() 中直接访问。 例如

此外,directive-helpers.js 模块包含一些辅助函数,这些函数作用于 Part 对象,并且可以用于动态地创建、插入和移动指令的 ChildPart 中的部分。

update() 的默认实现只是调用并返回 render() 的值。 如果您覆盖了 update() 并且仍然想调用 render() 来生成一个值,您需要显式地调用 render()

render() 参数作为数组传递到 update() 中。 您可以像这样将参数传递给 render()

虽然 update() 回调比 render() 回调更强大,但有一个重要的区别:当使用 @lit-labs/ssr 包进行服务器端渲染 (SSR) 时,只有 render() 方法在服务器上被调用。 为了与 SSR 兼容,指令应该从 render() 中返回值,并且只使用 update() 来处理需要访问 DOM 的逻辑。

有时指令可能没有任何新的内容要 Lit 渲染。 您可以通过从 update()render() 方法中返回 noChange 来表示这一点。 这与返回 undefined 不同,后者会导致 Lit 清除与指令关联的 Part。 返回 noChange 会将先前渲染的值保留在原位。

返回 noChange 有几个常见的原因

  • 根据输入值,没有任何新的内容要渲染。
  • update() 方法命令式地更新了 DOM。
  • 在异步指令中,对 update()render() 的调用可能会返回 noChange,因为还没有任何内容要渲染。

例如,指令可以跟踪传递给它的先前值,并执行自己的脏检查以确定指令的输出是否需要更新。 update()render() 方法可以返回 noChange 来表示指令的输出不需要重新渲染。

某些指令只在一个上下文中有效,例如属性表达式或子表达式。如果指令放置在错误的上下文中,则应抛出适当的错误。

例如,classMap 指令验证它仅在 AttributePart 中使用,并且仅用于 class 属性。

前面的示例指令是同步的:它们从其 render()/update() 生命周期的回调中同步地返回值,因此它们的结果在组件的 update() 回调期间写入 DOM。

有时,您希望指令能够异步更新 DOM——例如,如果它依赖于异步事件(如网络请求)。

要异步更新指令的结果,指令需要扩展 AsyncDirective 基类,该基类提供 setValue() API。setValue() 允许指令在模板的正常 update/render 周期之外将新值“推入”其模板表达式。

这是一个简单异步指令的示例,它呈现 Promise 值

这里,呈现的模板显示“等待 Promise 解析”,然后是 Promise 的解析值(无论何时解析)。

异步指令通常需要订阅外部资源。为了防止内存泄漏,异步指令在指令实例不再使用时应取消订阅或处置资源。为此,AsyncDirective 提供以下额外生命周期回调和 API

  • disconnected():在指令不再使用时调用。指令实例在三种情况下断开连接

    • 当包含指令的 DOM 树从 DOM 中删除时
    • 当指令的主机元素断开连接时
    • 当生成指令的表达式不再解析为同一个指令时。

    在指令收到 disconnected 回调后,它应该释放它可能在 updaterender 期间订阅的所有资源,以防止内存泄漏。

  • reconnected():当先前断开的指令被返回使用时调用。由于 DOM 子树可以暂时断开连接,然后在以后重新连接,因此断开的指令可能需要对重新连接做出反应。这种情况的示例包括 DOM 被删除并缓存以供以后使用,或者主机元素被移动导致断开连接和重新连接。reconnected() 回调应始终与 disconnected() 一起实现,以便将断开的指令恢复到其工作状态。

  • isConnected:反映指令的当前连接状态。

请注意,AsyncDirective 可以在断开连接时继续接收更新,前提是其包含的树被重新渲染。因此,update 和/或 render 应始终在订阅任何长期持有资源之前检查 this.isConnected 标志,以防止内存泄漏。

以下是一个指令的示例,该指令订阅 Observable 并适当地处理断开连接和重新连接