自定义指令

指令是可以通过自定义模板表达式的渲染方式来扩展 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() 回调接收两个参数:

  • 具有直接管理与表达式关联的 DOM 的 API 的 Part 对象。
  • 包含 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() 之间的区别

Permalink to "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 来表示指令的输出不需要重新渲染。

将指令限制为一种表达式类型

Permalink to "将指令限制为一种表达式类型"

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

例如,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 并适当处理断开连接和重新连接的指令示例: