自定义指令
指令是可以通过自定义模板表达式的渲染方式来扩展 Lit 的函数。指令非常有用且强大,因为它们可以保持状态、访问 DOM、在模板断开连接和重新连接时收到通知,以及在渲染调用之外独立更新表达式。
在模板中使用指令就像在模板表达式中调用函数一样简单:
html`<div>
${fancyDirective('some text')}
</div>`
Lit 附带了许多内置指令,如 repeat()
和 cache()
。用户也可以编写自己的自定义指令。
有两种类型的指令:
- 简单函数
- 基于类的指令
简单函数返回要渲染的值。它可以接受任意数量的参数,或者根本不接受参数。
export noVowels = (str) => str.replaceAll(/[aeiou]/ig,'x');
基于类的指令可以让你做简单函数无法做到的事情。使用基于类的指令可以:
- 直接访问渲染的 DOM(例如,添加、删除或重新排序渲染的 DOM 节点)。
- 在渲染之间保持状态。
- 在渲染调用之外异步更新 DOM。
- 在指令与 DOM 断开连接时清理资源
本页其余部分描述基于类的指令。
创建基于类的指令
Permalink to "创建基于类的指令"要创建基于类的指令:
- 实现一个继承自
Directive
类的指令类。 - 将你的类传递给
directive()
工厂函数,以创建可在 Lit 模板表达式中使用的指令函数。
import {Directive, directive} from 'lit/directive.js';
// 定义指令
class HelloDirective extends Directive {
render() {
return `Hello!`;
}
}
// 创建指令函数
const hello = directive(HelloDirective);
// 使用指令
const template = html`<div>${hello()}</div>`;
当评估此模板时,指令_函数_ (hello()
) 返回一个 DirectiveResult
对象,该对象指示 Lit 创建或更新指令_类_ (HelloDirective
) 的实例。然后,Lit 调用指令实例上的方法来运行其更新逻辑。
某些指令需要在正常更新周期之外异步更新 DOM。要创建_异步指令_,请继承 AsyncDirective
基类而不是 Directive
。有关详细信息,请参阅异步指令。
基于类的指令的生命周期
Permalink to "基于类的指令的生命周期"指令类有几个内置的生命周期方法:
- 类构造函数,用于一次性初始化。
render()
,用于声明式渲染。update()
,用于命令式 DOM 访问。
对于所有指令,你必须实现 render()
回调。实现 update()
是可选的。update()
的默认实现调用并返回 render()
的值。
异步指令,可以在正常更新周期之外更新 DOM,使用一些额外的生命周期回调。有关详细信息,请参阅异步指令。
一次性设置:constructor()
Permalink to "一次性设置:constructor()"当 Lit 在表达式中首次遇到 DirectiveResult
时,它将构造相应指令类的实例(导致指令的构造函数和任何类字段初始化器运行):
class MyDirective extends Directive {
// 类字段将被初始化一次,可用于在渲染之间保持状态
value = 0;
// 构造函数仅在指令首次在表达式中使用时运行
constructor(partInfo: PartInfo) {
super(partInfo);
console.log('MyDirective created');
}
...
}
class MyDirective extends Directive {
// 类字段将被初始化一次,可用于在渲染之间保持状态
value = 0;
// 构造函数仅在指令首次在表达式中使用时运行
constructor(partInfo) {
super(partInfo);
console.log('MyDirective created');
}
...
}
只要每次渲染在同一表达式中使用相同的指令函数,就会重用先前的实例,因此实例的状态在渲染之间保持不变。
构造函数接收单个 PartInfo
对象,该对象提供有关使用指令的表达式的元数据。对于设计为仅在特定类型表达式中使用的指令,这对于提供错误检查很有用(参见将指令限制为一种表达式类型)。
声明式渲染:render()
Permalink to "声明式渲染:render()"render()
方法应返回要渲染到 DOM 中的值。它可以返回任何可渲染的值,包括另一个 DirectiveResult
。
除了引用指令实例上的状态外,render()
方法还可以接受传递给指令函数的任意参数:
const template = html`<div>${myDirective(name, rank)}</div>`
为 render()
方法定义的参数决定了指令函数的签名:
class MaxDirective extends Directive {
maxValue = Number.MIN_VALUE;
// 定义一个 render 方法,可以接受参数:
render(value: number, minValue = Number.MIN_VALUE) {
this.maxValue = Math.max(value, this.maxValue, minValue);
return this.maxValue;
}
}
const max = directive(MaxDirective);
// 使用为 `render()` 定义的 `value` 和 `minValue` 参数调用指令:
const template = html`<div>${max(someNumber, 0)}</div>`;
class MaxDirective extends Directive {
maxValue = Number.MIN_VALUE;
// 定义一个 render 方法,可以接受参数:
render(value, minValue = Number.MIN_VALUE) {
this.maxValue = Math.max(value, this.maxValue, minValue);
return this.maxValue;
}
}
const max = directive(MaxDirective);
// 使用为 `render()` 定义的 `value` 和 `minValue` 参数调用指令:
const template = html`<div>${max(someNumber, 0)}</div>`;
命令式 DOM 访问:update()
Permalink to "命令式 DOM 访问:update()"在更高级的用例中,你的指令可能需要访问底层 DOM 并命令式地从中读取或修改它。你可以通过重写 update()
回调来实现这一点。
update()
回调接收两个参数:
- 具有直接管理与表达式关联的 DOM 的 API 的
Part
对象。 - 包含
render()
参数的数组。
你的 update()
方法应返回 Lit 可以渲染的内容,或者如果不需要重新渲染,则返回特殊值 noChange
。update()
回调非常灵活,但典型用途包括:
- 从 DOM 读取数据,并使用它生成要渲染的值。
- 使用
Part
对象上的element
或parentNode
引用命令式地更新 DOM。在这种情况下,update()
通常返回noChange
,表示 Lit 不需要采取任何进一步操作来渲染指令。
Parts
Permalink to "Parts"每个表达式位置都有自己特定的 Part
对象:
ChildPart
用于 HTML 子位置中的表达式。AttributePart
用于 HTML 属性值位置中的表达式。BooleanAttributePart
用于布尔属性值(名称以?
为前缀)中的表达式。EventPart
用于事件监听器位置(名称以@
为前缀)中的表达式。PropertyPart
用于属性值位置(名称以.
为前缀)中的表达式。ElementPart
用于元素标签上的表达式。
除了 PartInfo
中包含的特定于部分的元数据外,所有 Part
类型都提供对与表达式关联的 DOM element
(或者在 ChildPart
的情况下为 parentNode
)的访问,可以在 update()
中直接访问。例如:
// 将父元素的属性名称渲染到 textContent
class AttributeLogger extends Directive {
attributeNames = '';
update(part: ChildPart) {
this.attributeNames = (part.parentNode as Element).getAttributeNames?.().join(' ');
return this.render();
}
render() {
return this.attributeNames;
}
}
const attributeLogger = directive(AttributeLogger);
const template = html`<div a b>${attributeLogger()}</div>`;
// Renders: `<div a b>a b</div>`
// 将父元素的属性名称渲染到 textContent
class AttributeLogger extends Directive {
attributeNames = '';
update(part) {
this.attributeNames = part.parentNode.getAttributeNames?.().join(' ');
return this.render();
}
render() {
return this.attributeNames;
}
}
const attributeLogger = directive(AttributeLogger);
const template = html`<div a b>${attributeLogger()}</div>`;
// Renders: `<div a b>a b</div>`
此外,directive-helpers.js
模块包含许多作用于 Part
对象的辅助函数,可用于在指令的 ChildPart
内动态创建、插入和移动部分。
从 update() 调用 render()
Permalink to "从 update() 调用 render()"update()
的默认实现只是调用并返回 render()
的值。如果你重写了 update()
并且仍然想调用 render()
来生成值,你需要显式调用 render()
。
render()
的参数作为数组传递给 update()
。你可以像这样将参数传递给 render()
:
class MyDirective extends Directive {
update(part: Part, [fish, bananas]: DirectiveParameters<this>) {
// ...
return this.render(fish, bananas);
}
render(fish: number, bananas: number) { ... }
}
class MyDirective extends Directive {
update(part, [fish, bananas]) {
// ...
return this.render(fish, bananas);
}
render(fish, bananas) { ... }
}
update() 和 render() 之间的区别
Permalink to "update() 和 render() 之间的区别"虽然 update()
回调比 render()
回调更强大,但有一个重要的区别:当使用 @lit-labs/ssr
包进行服务器端渲染(SSR)时,只有 render()
方法在服务器上被调用。为了与 SSR 兼容,指令应该从 render()
返回值,并且只将 update()
用于需要访问 DOM 的逻辑。
表示无变化
Permalink to "表示无变化"有时,指令可能没有新内容供 Lit 渲染。你可以通过从 update()
或 render()
方法返回 noChange
来表示这一点。这与返回 undefined
不同,后者会导致 Lit 清除与指令关联的 Part
。返回 noChange
会保留先前渲染的值不变。
返回 noChange
的几个常见原因:
- 基于输入值,没有新内容需要渲染。
update()
方法已命令式地更新了 DOM。- 在异步指令中,调用
update()
或render()
可能返回noChange
,因为_暂时_没有内容需要渲染。
例如,指令可以跟踪传递给它的先前值,并执行自己的脏检查以确定指令的输出是否需要更新。update()
或 render()
方法可以返回 noChange
来表示指令的输出不需要重新渲染。
import {Directive} from 'lit/directive.js';
import {noChange} from 'lit';
class CalculateDiff extends Directive {
a?: string;
b?: string;
render(a: string, b: string) {
if (this.a !== a || this.b !== b) {
this.a = a;
this.b = b;
// 昂贵且花哨的文本差异算法
return calculateDiff(a, b);
}
return noChange;
}
}
import {Directive} from 'lit/directive.js';
import {noChange} from 'lit';
class CalculateDiff extends Directive {
render(a, b) {
if (this.a !== a || this.b !== b) {
this.a = a;
this.b = b;
// 昂贵且花哨的文本差异算法
return calculateDiff(a, b);
}
return noChange;
}
}
将指令限制为一种表达式类型
Permalink to "将指令限制为一种表达式类型"某些指令只在一种上下文中有用,例如属性表达式或子表达式。如果放置在错误的上下文中,指令应抛出适当的错误。
例如,classMap
指令验证它仅用于 AttributePart
并且仅用于 class
属性:
class ClassMap extends Directive {
constructor(partInfo: PartInfo) {
super(partInfo);
if (
partInfo.type !== PartType.ATTRIBUTE ||
partInfo.name !== 'class'
) {
throw new Error('The `classMap` directive must be used in the `class` attribute');
}
}
...
}
class ClassMap extends Directive {
constructor(partInfo) {
super(partInfo);
if (
partInfo.type !== PartType.ATTRIBUTE ||
partInfo.name !== 'class'
) {
throw new Error('The `classMap` directive must be used in the `class` attribute');
}
}
...
}
前面示例中的指令是同步的:它们从 render()
/update()
生命周期回调中同步返回值,因此它们的结果在组件的 update()
回调期间被写入 DOM。
有时,你希望指令能够异步更新 DOM — 例如,如果它依赖于异步事件,如网络请求。
要异步更新指令的结果,指令需要扩展 AsyncDirective
基类,它提供了 setValue()
API。setValue()
允许指令在模板的正常 update
/render
周期之外"推送"新值到其模板表达式中。
下面是一个简单的异步指令示例,它渲染 Promise 值:
class ResolvePromise extends AsyncDirective {
render(promise: Promise<unknown>) {
Promise.resolve(promise).then((resolvedValue) => {
// 同步渲染:
this.setValue(resolvedValue);
});
// 同步渲染:
return `Waiting for promise to resolve`;
}
}
export const resolvePromise = directive(ResolvePromise);
class ResolvePromise extends AsyncDirective {
render(promise) {
Promise.resolve(promise).then((resolvedValue) => {
// 同步渲染:
this.setValue(resolvedValue);
});
// 同步渲染:
return `Waiting for promise to resolve`;
}
}
export const resolvePromise = directive(ResolvePromise);
这里,渲染的模板显示"等待 promise 解决",随后当 promise 解决时显示其解决值。
异步指令经常需要订阅外部资源。为了防止内存泄漏,异步指令应该在指令实例不再使用时取消订阅或释放资源。为此,AsyncDirective
提供了以下额外的生命周期回调和 API:
disconnected()
:当指令不再使用时调用。指令实例在三种情况下会断开连接:- 当包含指令的 DOM 树从 DOM 中移除时
- 当指令的宿主元素断开连接时
- 当产生指令的表达式不再解析为相同的指令时
在指令收到
disconnected
回调后,它应该释放在update
或render
期间可能订阅的所有资源,以防止内存泄漏。reconnected()
:当先前断开连接的指令重新使用时调用。由于 DOM 子树可能暂时断开连接然后稍后重新连接,断开连接的指令可能需要对重新连接做出反应。例如,当 DOM 被移除并缓存以供以后使用,或者当宿主元素被移动导致断开连接和重新连接时。reconnected()
回调应始终与disconnected()
一起实现,以便将断开连接的指令恢复到工作状态。isConnected
:反映指令当前的连接状态。
请注意,即使 AsyncDirective
已断开连接,如果其包含的树被重新渲染,它仍可能继续接收更新。因此,update
和/或 render
应始终在订阅任何长期持有的资源之前检查 this.isConnected
标志,以防止内存泄漏。
下面是一个订阅 Observable
并适当处理断开连接和重新连接的指令示例:
class ObserveDirective extends AsyncDirective {
observable: Observable<unknown> | undefined;
unsubscribe: (() => void) | undefined;
// 当 Observable 变化时,取消订阅旧的并订阅新的
render(observable: Observable<unknown>) {
if (this.observable !== observable) {
this.unsubscribe?.();
this.observable = observable
if (this.isConnected) {
this.subscribe(observable);
}
}
return noChange;
}
// 订阅 Observable,在每次值变化时调用指令的异步 setValue API
subscribe(observable: Observable<unknown>) {
this.unsubscribe = observable.subscribe((v: unknown) => {
this.setValue(v);
});
}
// 当指令从 DOM 断开连接时,取消订阅以确保指令实例可以被垃圾回收
disconnected() {
this.unsubscribe!();
}
// 如果包含指令的子树被断开连接后又重新连接,重新订阅使指令可再次操作
reconnected() {
this.subscribe(this.observable!);
}
}
export const observe = directive(ObserveDirective);
class ObserveDirective extends AsyncDirective {
// 当 Observable 变化时,取消订阅旧的并订阅新的
render(observable) {
if (this.observable !== observable) {
this.unsubscribe?.();
this.observable = observable
if (this.isConnected) {
this.subscribe(observable);
}
}
return noChange;
}
// 订阅 Observable,在每次值变化时调用指令的异步 setValue API
subscribe(observable) {
this.unsubscribe = observable.subscribe((v) => {
this.setValue(v);
});
}
// 当指令从 DOM 断开连接时,取消订阅以确保指令实例可以被垃圾回收
disconnected() {
this.unsubscribe();
}
// 如果包含指令的子树被断开连接后又重新连接,重新订阅使指令可再次操作
reconnected() {
this.subscribe(this.observable);
}
}
export const observe = directive(ObserveDirective);