自定义指令
指令是函数,可以通过自定义模板表达式的渲染方式来扩展 Lit。 指令非常有用且强大,因为它们可以是有状态的,可以访问 DOM,可以在模板断开连接和重新连接时收到通知,并且可以在渲染调用之外独立更新表达式。
在您的模板中使用指令就像在模板表达式中调用函数一样简单
html`<div>
${fancyDirective('some text')}
</div>`
Lit 附带了一些 内置指令,例如 repeat()
和 cache()
。 用户也可以编写自己的自定义指令。
指令有两种类型
- 简单函数
- 基于类的指令
简单函数返回一个值进行渲染。 它可以接受任意数量的参数,或者根本不接受参数。
export noVowels = (str) => str.replaceAll(/[aeiou]/ig,'x');
基于类的指令允许您做简单函数无法做到的事情。 使用基于类的指令来
- 直接访问渲染的 DOM(例如,添加、删除或重新排序渲染的 DOM 节点)。
- 在渲染之间保留状态。
- 在渲染调用之外异步更新 DOM。
- 在指令与 DOM 断开连接时清理资源
本页的其余部分描述了基于类的指令。
创建基于类的指令
“创建基于类的指令”的永久链接要创建基于类的指令
- 将指令实现为扩展
Directive
类的类。 - 将您的类传递给
directive()
工厂以创建一个指令函数,该函数可以在 Lit 模板表达式中使用。
import {Directive, directive} from 'lit/directive.js';
// Define directive
class HelloDirective extends Directive {
render() {
return `Hello!`;
}
}
// Create the directive function
const hello = directive(HelloDirective);
// Use directive
const template = html`<div>${hello()}</div>`;
当此模板被评估时,指令函数(hello()
)将返回一个 DirectiveResult
对象,该对象指示 Lit 创建或更新指令类(HelloDirective
)的实例。 然后 Lit 调用指令实例上的方法来运行其更新逻辑。
某些指令需要在正常的更新周期之外异步更新 DOM。 要创建一个异步指令,请扩展 AsyncDirective
基类而不是 Directive
。 有关详细信息,请参阅 异步指令。
基于类的指令的生命周期
“基于类的指令的生命周期”的永久链接指令类有一些内置的生命周期方法
- 类构造函数,用于一次性初始化。
render()
,用于声明式渲染。update()
,用于命令式 DOM 访问。
您必须为所有指令实现 render()
回调。 实现 update()
是可选的。 update()
的默认实现调用并返回 render()
的值。
异步指令可以在正常的更新周期之外更新 DOM,它们使用一些额外的生命周期回调。 有关详细信息,请参阅 异步指令。
一次性设置:constructor()
“一次性设置:constructor()”的永久链接当 Lit 首次在表达式中遇到 DirectiveResult
时,它将构造一个对应指令类的实例(导致指令的构造函数和任何类字段初始化器运行)
class MyDirective extends Directive {
// Class fields will be initialized once and can be used to persist
// state between renders
value = 0;
// Constructor is only run the first time a given directive is used
// in an expression
constructor(partInfo: PartInfo) {
super(partInfo);
console.log('MyDirective created');
}
...
}
class MyDirective extends Directive {
// Class fields will be initialized once and can be used to persist
// state between renders
value = 0;
// Constructor is only run the first time a given directive is used
// in an expression
constructor(partInfo) {
super(partInfo);
console.log('MyDirective created');
}
...
}
只要在每次渲染时在同一个表达式中使用同一个指令函数,就会重用之前的实例,因此实例的状态在渲染之间保留。
构造函数接收一个 PartInfo
对象,它提供有关使用指令的表达式的元数据。 这对于在指令旨在仅用于特定类型表达式的情况下提供错误检查很有用(请参阅 将指令限制为一种表达式类型)。
声明式渲染:render()
“声明式渲染:render()”的永久链接render()
方法应该返回渲染到 DOM 中的值。 它可以返回任何可渲染的值,包括另一个 DirectiveResult
。
除了引用指令实例上的状态外,render()
方法还可以接受传递给指令函数的任意参数
const template = html`<div>${myDirective(name, rank)}</div>`
为 render()
方法定义的参数确定指令函数的签名
class MaxDirective extends Directive {
maxValue = Number.MIN_VALUE;
// Define a render method, which may accept arguments:
render(value: number, minValue = Number.MIN_VALUE) {
this.maxValue = Math.max(value, this.maxValue, minValue);
return this.maxValue;
}
}
const max = directive(MaxDirective);
// Call the directive with `value` and `minValue` arguments defined for `render()`:
const template = html`<div>${max(someNumber, 0)}</div>`;
class MaxDirective extends Directive {
maxValue = Number.MIN_VALUE;
// Define a render method, which may accept arguments:
render(value, minValue = Number.MIN_VALUE) {
this.maxValue = Math.max(value, this.maxValue, minValue);
return this.maxValue;
}
}
const max = directive(MaxDirective);
// Call the directive with `value` and `minValue` arguments defined for `render()`:
const template = html`<div>${max(someNumber, 0)}</div>`;
命令式 DOM 访问:update()
“命令式 DOM 访问:update()”的永久链接在更高级的用例中,您的指令可能需要访问底层 DOM 并命令式地从中读取或修改它。 您可以通过覆盖 update()
回调来实现这一点。
update()
回调接收两个参数
- 一个
Part
对象,它提供了一个直接管理与表达式关联的 DOM 的 API。 - 包含
render()
参数的数组。
您的 update()
方法应该返回 Lit 可以渲染的东西,或者如果不需要重新渲染,则返回特殊值 noChange
。 update()
回调非常灵活,但典型用途包括
- 从 DOM 中读取数据,并使用它生成要渲染的值。
- 使用
Part
对象上的element
或parentNode
引用命令式地更新 DOM。 在这种情况下,update()
通常返回noChange
,表示 Lit 不需要采取任何进一步的操作来渲染指令。
每个表达式位置都有其自己的特定 Part
对象
ChildPart
用于 HTML 子位置中的表达式。AttributePart
用于 HTML 属性值位置中的表达式。BooleanAttributePart
用于布尔属性值(名称以?
为前缀)位置中的表达式。EventPart
用于事件侦听器位置(名称以@
为前缀)中的表达式。PropertyPart
用于属性值位置(名称以.
为前缀)中的表达式。ElementPart
用于元素标签上的表达式。
除了 PartInfo
中包含的特定于部分的元数据外,所有 Part
类型都提供对与表达式关联的 DOM element
(或在 ChildPart
的情况下为 parentNode
)的访问权限,这些元素可以在 update()
中直接访问。 例如
// Renders attribute names of parent element to 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>`
// Renders attribute names of parent element to 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()
“从 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() 之间的区别
“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
来表示指令的输出不需要重新渲染。
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;
// Expensive & fancy text diffing algorithm
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;
// Expensive & fancy text diffing algorithm
return calculateDiff(a, b);
}
return noChange;
}
}
将指令限制为一种表达式类型
指向“将指令限制为一种表达式类型”的永久链接某些指令只在一个上下文中有效,例如属性表达式或子表达式。如果指令放置在错误的上下文中,则应抛出适当的错误。
例如,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) => {
// Rendered asynchronously:
this.setValue(resolvedValue);
});
// Rendered synchronously:
return `Waiting for promise to resolve`;
}
}
export const resolvePromise = directive(ResolvePromise);
class ResolvePromise extends AsyncDirective {
render(promise) {
Promise.resolve(promise).then((resolvedValue) => {
// Rendered asynchronously:
this.setValue(resolvedValue);
});
// Rendered synchronously:
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;
// When the observable changes, unsubscribe to the old one and
// subscribe to the new one
render(observable: Observable<unknown>) {
if (this.observable !== observable) {
this.unsubscribe?.();
this.observable = observable
if (this.isConnected) {
this.subscribe(observable);
}
}
return noChange;
}
// Subscribes to the observable, calling the directive's asynchronous
// setValue API each time the value changes
subscribe(observable: Observable<unknown>) {
this.unsubscribe = observable.subscribe((v: unknown) => {
this.setValue(v);
});
}
// When the directive is disconnected from the DOM, unsubscribe to ensure
// the directive instance can be garbage collected
disconnected() {
this.unsubscribe!();
}
// If the subtree the directive is in was disconnected and subsequently
// re-connected, re-subscribe to make the directive operable again
reconnected() {
this.subscribe(this.observable!);
}
}
export const observe = directive(ObserveDirective);
class ObserveDirective extends AsyncDirective {
// When the observable changes, unsubscribe to the old one and
// subscribe to the new one
render(observable) {
if (this.observable !== observable) {
this.unsubscribe?.();
this.observable = observable
if (this.isConnected) {
this.subscribe(observable);
}
}
return noChange;
}
// Subscribes to the observable, calling the directive's asynchronous
// setValue API each time the value changes
subscribe(observable) {
this.unsubscribe = observable.subscribe((v) => {
this.setValue(v);
});
}
// When the directive is disconnected from the DOM, unsubscribe to ensure
// the directive instance can be garbage collected
disconnected() {
this.unsubscribe();
}
// If the subtree the directive is in was disconneted and subsequently
// re-connected, re-subscribe to make the directive operable again
reconnected() {
this.subscribe(this.observable);
}
}
export const observe = directive(ObserveDirective);