你正在查看 Lit 的旧版本文档。点击 这里查看最新版本。

响应式控制器

Lit 2 引入了一种新的代码复用和组合概念,称为 响应式控制器

响应式控制器是一个可以挂接到组件响应式更新周期的对象。控制器可以将与某个功能相关的状态和行为打包在一起,使其可以在多个组件定义中复用。

你可以使用控制器来实现需要自身状态和访问组件生命周期的功能,例如:

  • 处理全局事件(如鼠标事件)
  • 管理异步任务(如通过网络获取数据)
  • 运行动画

响应式控制器允许你通过组合更小的、本身不是组件的部分来构建组件。它们可以被看作是可复用的、部分的组件定义,拥有自己的标识和状态。

响应式控制器在许多方面类似于类混入。主要区别在于它们拥有自己的标识,不会添加到组件的原型上,这有助于约束它们的 API,并且允许你在每个宿主组件上使用多个控制器实例。详情请参阅控制器与混入

每个控制器都有自己的创建 API,但通常你需要创建一个实例并将其存储在组件中:

class MyElement extends LitElement {
private clock = new ClockController(this, 1000);
}

与控制器实例关联的组件称为宿主组件。

控制器实例将自身注册为接收宿主组件的生命周期回调,并在控制器有新数据需要渲染时触发宿主更新。这就是 ClockController 示例如何定期渲染当前时间的方式。

控制器通常会暴露一些功能供宿主的 render() 方法使用。例如,许多控制器会有一些状态,比如当前值:

render() {
return html`
<div>Current time: ${this.clock.value}</div>
`;
}

由于每个控制器都有自己的 API,请参考特定控制器的文档了解如何使用它们。

响应式控制器是一个与宿主组件关联的对象,它实现一个或多个宿主生命周期回调或与其宿主进行交互。它可以通过多种方式实现,但我们将重点介绍使用 JavaScript 类的方式,使用构造函数进行初始化,使用方法实现生命周期。

控制器通过调用 host.addController(this) 向其宿主组件注册自身。通常控制器会存储对宿主组件的引用,以便后续与其交互。

class ClockController implements ReactiveController {
private host: ReactiveControllerHost;

constructor(host: ReactiveControllerHost) {
// 存储对宿主的引用
this.host = host;
// 注册以接收生命周期更新
host.addController(this);
}
}
class ClockController {
constructor(host) {
// 存储对宿主的引用
this.host = host;
// 注册以接收生命周期更新
host.addController(this);
}
}

你可以添加其他构造函数参数用于一次性配置。

class ClockController implements ReactiveController {
private host: ReactiveControllerHost;
timeout: number

constructor(host: ReactiveControllerHost, timeout: number) {
this.host = host;
this.timeout = timeout;
host.addController(this);
}
class ClockController {
constructor(host, timeout) {
this.host = host;
this.timeout = timeout;
host.addController(this);
}

一旦你的控制器向宿主组件注册完毕,你就可以向控制器添加生命周期回调和其他类字段及方法,以实现所需的状态和行为。

响应式控制器的生命周期定义在 ReactiveController 接口中,是响应式更新周期的一个子集。LitElement 在其生命周期回调期间会调用任何已安装的控制器。这些回调是可选的。

  • hostConnected()
    • 当宿主连接时调用。
    • 在创建 renderRoot 之后调用,因此此时 shadow root 已经存在。
    • 用于设置事件监听器、观察器等。
  • hostUpdate()
    • 在宿主的 update()render() 方法之前调用。
    • 用于在 DOM 更新前读取 DOM(例如用于动画)。
  • hostUpdated()
    • 在更新之后、宿主的 updated() 方法之前调用。
    • 用于在 DOM 修改后读取 DOM(例如用于动画)。
  • hostDisconnected()
    • 当宿主断开连接时调用。
    • 用于清理在 hostConnected() 中添加的内容,如事件监听器和观察器。

更多信息请参阅响应式更新周期

响应式控制器宿主实现了一小组用于添加控制器和请求更新的 API,并负责调用其控制器的生命周期方法。

这是控制器宿主公开的最小 API:

  • addController(controller: ReactiveController)
  • removeController(controller: ReactiveController)
  • requestUpdate()
  • updateComplete: Promise<boolean>

你也可以创建特定于 HTMLElementReactiveElementLitElement 的控制器,要求更多这些 API;或者甚至是绑定到特定元素类或其他接口的控制器。

LitElementReactiveElement 是控制器宿主,但宿主也可以是其他对象,如来自其他 Web Components 库的基类、框架中的组件,或者其他控制器。

控制器也可以由其他控制器组成。要实现这一点,创建一个子控制器并将宿主转发给它。

class DualClockController implements ReactiveController {
private clock1: ClockController;
private clock2: ClockController;

constructor(host: ReactiveControllerHost, delay1: number, delay2: number) {
this.clock1 = new ClockController(host, delay1);
this.clock2 = new ClockController(host, delay2);
}

get time1() { return this.clock1.value; }
get time2() { return this.clock2.value; }
}
class DualClockController {
constructor(host, delay1, delay2) {
this.clock1 = new ClockController(host, delay1);
this.clock2 = new ClockController(host, delay2);
}

get time1() { return this.clock1.value; }
get time2() { return this.clock2.value; }
}

将控制器与指令结合使用可以是一种非常强大的技术,特别是对于需要在渲染前后进行工作的指令(如动画指令);或者需要引用模板中特定元素的控制器。

控制器与指令结合使用有两种主要模式:

  • 控制器指令。这些指令本身就是控制器,以便挂接到宿主生命周期中。
  • 拥有指令的控制器。这些控制器创建一个或多个指令供宿主模板使用。

有关编写指令的更多信息,请参阅自定义指令

响应式控制器不必作为实例字段存储在宿主上。任何使用 addController() 添加到宿主的对象都是控制器。特别地,指令也可以是控制器。这使得指令能够挂接到宿主生命周期中。

指令不必是独立的函数,它们也可以是其他对象(如控制器)上的方法。在控制器需要对模板中某个元素有特定引用时,这非常有用。

例如,假设有一个 ResizeController,它允许你使用 ResizeObserver 观察元素的大小。要使其工作,我们既需要一个 ResizeController 实例,也需要一个放置在我们要观察的元素上的指令:

class MyElement extends LitElement {
private _textSize = new ResizeController(this);

render() {
return html`
<textarea ${this._textSize.observe()}></textarea>
<p>The width is ${this._textSize.contentRect?.width}</p>
`;
}
}
class MyElement extends LitElement {
_textSize = new ResizeController(this);

render() {
return html`
<textarea ${this._textSize.observe()}></textarea>
<p>The width is ${this._textSize.contentRect?.width}</p>
`;
}
}

要实现这一点,你需要创建一个指令并从一个方法中调用它:

class ResizeDirective {
/* ... */
}
const resizeDirective = directive(ResizeDirective);

export class ResizeController {
/* ... */
observe() {
// 传递对控制器的引用,以便指令可以在大小变化时通知控制器。
return resizeDirective(this);
}
}

TO DO

  • 审查并清理此示例

响应式控制器非常通用,具有非常广泛的可能使用场景。它们特别适合将组件连接到外部资源,如用户输入、状态管理或远程 API。以下是一些常见的使用场景。

响应式控制器可用于连接外部输入。例如,键盘和鼠标事件、resize 观察器或 mutation 观察器。控制器可以提供输入的当前值用于渲染,并在值变化时请求宿主更新。

此示例展示了控制器如何在其宿主连接和断开连接时执行设置和清理工作,以及在输入变化时请求更新:

异步任务(如长时间运行的计算或网络 I/O)通常具有随时间变化的状态,当任务状态发生变化(完成、出错等)时需要通知宿主。

控制器是将任务执行和状态打包在一起的好方法,便于在组件内使用。编写为控制器的任务通常具有宿主可以设置的输入,以及宿主可以渲染的输出。

@lit-labs/task 包含一个通用的 Task 控制器,它可以从宿主获取输入、执行任务函数,并根据任务状态渲染不同的模板。

你可以使用 Task 创建一个自定义控制器,其 API 为你的特定任务量身定制。这里我们将 Task 包装在一个 NamesController 中,它可以从演示 REST API 获取指定列表中的一个名称。NameController 暴露一个 kind 属性作为输入,以及一个 render() 方法,该方法可以根据任务状态渲染四种模板之一。任务逻辑以及它如何更新宿主都被抽象在宿主组件之外。

TO DO

  • 动画