响应式控制器

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

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

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

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

响应式控制器在许多方面与类混入(mixin)相似。主要区别在于它们有自己的标识,不会添加到组件的原型中,这有助于包含它们的 API,并允许你在每个宿主组件中使用多个控制器实例。更多详情请参见控制器和混入

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

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

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

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

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

render() {
return html`
<div>当前时间:${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 DOM 根节点已存在。
    • 适用于设置事件监听器、观察器等。
  • hostUpdate()
    • 在宿主的 update()render() 方法之前调用。
    • 适用于在 DOM 更新前读取它(例如用于动画)。
  • hostUpdated()
    • 在更新之后、宿主的 updated() 方法之前调用。
    • 适用于在 DOM 修改后读取它(例如用于动画)。
  • hostDisconnected()
    • 当宿主断开连接时调用。
    • 适用于清理在 hostConnected() 中添加的内容,如事件监听器和观察器。

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

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

这是控制器宿主暴露的最小 API:

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

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

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

基于其他控制器构建控制器

Permalink to "基于其他控制器构建控制器"

控制器也可以由其他控制器组合而成。为此,创建一个子控制器并将宿主转发给它。

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>宽度为 ${this._textSize.contentRect?.width}</p>
`;
}
}
class MyElement extends LitElement {
_textSize = new ResizeController(this);

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

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

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

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

TO DO

  • 审查和清理此示例

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

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

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

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

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

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

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

TO DO

  • 动画