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

生命周期

Lit 组件使用标准的自定义元素生命周期方法。此外,Lit 引入了响应式更新周期,当响应式属性发生变化时将更改渲染到 DOM。

Lit 组件是标准的自定义元素,继承了自定义元素的生命周期方法。有关自定义元素生命周期的信息,请参阅 MDN 上的使用生命周期回调

如果你需要自定义任何标准自定义元素生命周期方法,请确保调用 super 实现(如 super.connectedCallback()),以保持标准 Lit 功能的正常运行。

在元素被创建时调用。此外,当现有元素被升级时也会调用,这发生在自定义元素的定义在元素已经存在于 DOM 中之后才被加载时。

使用 requestUpdate() 方法请求一次异步更新,因此当 Lit 组件被升级时,它会立即执行一次更新。

保存已经在元素上设置的任何属性。这确保在升级之前设置的值会被保留,并正确覆盖组件设置的默认值。

执行必须在第一次更新之前完成的一次性初始化任务。例如,当不使用装饰器时,可以在构造函数中设置属性的默认值,如在静态 properties 字段中声明属性所示。

constructor() {
super();
this.foo = 'foo';
this.bar = 'bar';
}

当组件被添加到文档的 DOM 时调用。

Lit 在元素连接后启动第一次元素更新周期。作为渲染的准备,Lit 还确保 renderRoot(通常是其 shadowRoot)被创建。

一旦元素至少连接到文档一次,组件更新将在不考虑元素连接状态的情况下继续进行。

connectedCallback() 中,你应该设置只应在元素连接到文档时发生的任务。其中最常见的是向元素外部的节点添加事件监听器,如添加到窗口的 keydown 事件处理器。通常,在 connectedCallback() 中所做的任何事情都应在元素断开连接时撤销——例如,移除窗口上的事件监听器以防止内存泄漏。

connectedCallback() {
super.connectedCallback()
window.addEventListener('keydown', this._handleKeydown);
}

当组件从文档的 DOM 中移除时调用。

暂停响应式更新周期。当元素重新连接时恢复。

此回调是元素可能不再被使用的主要信号;因此,disconnectedCallback() 应确保没有任何东西持有对元素的引用(如添加到元素外部节点的事件监听器),以便它可以被垃圾回收。因为元素在断开连接后可能会被重新连接,例如在 DOM 中移动元素或缓存的情况下,任何这样的引用或监听器可能需要通过 connectedCallback() 重新建立,以使元素在这些场景中继续按预期运行。例如,移除添加到元素外部节点(如窗口的 keydown 事件处理器)的事件监听器。

disconnectedCallback() {
super.disconnectedCallback()
window.removeEventListener('keydown', this._handleKeydown);
}

无需移除内部事件监听器。 你不需要移除添加在组件自身 DOM 上的事件监听器——包括在模板中声明式添加的那些。与外部事件监听器不同,这些不会阻止组件被垃圾回收。

当元素的 observedAttributes 之一发生变化时调用。

Lit 使用此回调将 attribute 的变化同步到响应式属性。具体来说,当 attribute 被设置时,对应的属性也会被设置。Lit 还自动设置元素的 observedAttributes 数组,使其与组件的响应式属性列表匹配。

你很少需要实现此回调。

当组件被移动到新文档时调用。

请注意 adoptedCallback 没有 polyfill。

Lit 对此回调没有默认行为。

此回调只应在元素行为需要随文档变化而变化的高级场景中使用。

除了标准的自定义元素生命周期外,Lit 组件还实现了响应式更新周期。

响应式更新周期在响应式属性发生变化或显式调用 requestUpdate() 方法时触发。Lit 异步执行更新,因此属性变化是批量处理的——如果在更新请求之后、更新开始之前又有更多属性发生变化,所有这些变化都会被捕获在同一次更新中。

更新发生在微任务时间点,这意味着它们在浏览器将下一帧绘制到屏幕之前发生。有关浏览器时间的更多信息,请参阅 Jake Archibald 关于微任务的文章

从高层来看,响应式更新周期是:

  1. 当一个或多个属性发生变化或调用 requestUpdate() 时,安排一次更新。
  2. 在下一帧绘制之前执行更新。
    1. 设置反射 attribute。
    2. 调用组件的 render 方法以更新其内部 DOM。
  3. 更新完成,updateComplete Promise 被解决。

更详细地说,如下所示:

更新前

更新中

更新后

许多响应式更新方法接收一个 Map 类型的已更改属性映射。Map 的键是属性名称,其值是之前的属性值。你始终可以使用 this.propertythis[property] 来查找当前属性值。

changedProperties 的 TypeScript 类型

Permalink to "changedProperties 的 TypeScript 类型"

如果你使用 TypeScript 并且希望对 changedProperties 映射进行强类型检查,你可以使用 PropertyValues<this>,它会为每个属性名称推断正确的类型。

import {LitElement, html, PropertyValues} from 'lit';
...
shouldUpdate(changedProperties: PropertyValues<this>) {
...
}

如果你不太关心强类型——或者你只检查属性名称,不检查之前的值——你可以使用限制较少的类型,如 Map<string, any>

请注意,PropertyValues<this> 不识别 protectedprivate 属性。如果你检查任何 protectedprivate 属性,你需要使用限制较少的类型。

在更新期间(直到并包括 render() 方法)更改属性会更新 changedProperties 映射,但不会触发新的更新。在 render() 之后更改属性(例如,在 updated() 方法中)会触发新的更新周期,已更改的属性会被添加到新的 changedProperties 映射中以供下一个周期使用。

当响应式属性发生变化或调用 requestUpdate() 方法时,会触发更新。由于更新是异步执行的,在更新执行之前发生的任何和所有变化只会导致一次更新

当设置响应式属性时调用。默认情况下 hasChanged() 执行严格相等检查,如果返回 true,则安排一次更新。详见配置 hasChanged()

调用 requestUpdate() 来安排一次显式更新。当需要在与属性无关的内容发生变化时让元素更新和渲染,这会很有用。例如,一个计时器组件可能每秒调用一次 requestUpdate()

connectedCallback() {
super.connectedCallback();
this._timerInterval = setInterval(() => this.requestUpdate(), 1000);
}

disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this._timerInterval);
}

已更改属性的列表存储在一个 changedProperties 映射中,该映射会传递给后续的生命周期方法。映射的键是属性名称,其值是之前的属性值。

你也可以在调用 requestUpdate() 时选择性地传递属性名称和之前的值,这些会被存储在 changedProperties 映射中。如果你为属性实现了自定义 getter 和 setter,这会很有用。有关实现自定义 getter 和 setter 的更多信息,请参阅响应式属性

this.requestUpdate('state', this._previousState);

当执行更新时,会调用 performUpdate() 方法。此方法会调用其他一些生命周期方法。

在组件更新期间发生的通常会触发更新的任何变化都不会安排新的更新。这样做是为了在更新过程中可以计算属性值。在更新期间更改的属性会被反映在 changedProperties 映射中,因此后续的生命周期方法可以对这些变化做出反应。

调用以确定是否需要更新周期。

参数changedPropertiesMap,键是已更改属性的名称,值是对应的之前的值。
更新?否。此方法内的属性变化不会触发元素更新。
需要调用 super?不需要。
在服务器上调用?否。

如果 shouldUpdate() 返回 true(默认行为),则更新正常进行。如果返回 false,则不会调用更新周期的其余部分,但 updateComplete Promise 仍然会被解决。

你可以实现 shouldUpdate() 来指定哪些属性变化应导致更新。使用 changedProperties 映射来比较当前值和之前的值。

shouldUpdate(changedProperties: Map<string, any>) {
// 仅在 prop1 变化时更新元素
return changedProperties.has('prop1');
}
shouldUpdate(changedProperties) {
// 仅在 prop1 变化时更新元素
return changedProperties.has('prop1');
}

update() 之前调用,用于计算更新期间所需的值。

参数changedPropertiesMap,键是已更改属性的名称,值是对应的之前的值。
更新?否。此方法内的属性变化不会触发元素更新。
需要调用 super?不需要。
在服务器上调用?是。

实现 willUpdate() 来计算依赖于其他属性并在更新过程的其余部分使用的属性值。

willUpdate(changedProperties: PropertyValues<this>) {
// 对于昂贵的计算,只需要检查变化的属性
if (changedProperties.has('firstName') || changedProperties.has('lastName')) {
this.sha = computeSHA(`${this.firstName} ${this.lastName}`);
}
}

render() {
return html`SHA: ${this.sha}`;
}
willUpdate(changedProperties) {
// 对于昂贵的计算,只需要检查变化的属性
if (changedProperties.has('firstName') || changedProperties.has('lastName')) {
this.sha = computeSHA(`${this.firstName} ${this.lastName}`);
}
}

render() {
return html`SHA: ${this.sha}`;
}

调用以更新组件的 DOM。

参数changedPropertiesMap,键是已更改属性的名称,值是对应的之前的值。
更新?否。此方法内的属性变化不会触发元素更新。
需要调用 super?是。不调用 super,元素的属性和模板将不会更新。
在服务器上调用?否。

将属性值反射到 attribute 并调用 render() 来更新组件的内部 DOM。

通常,你不需要实现此方法。

update() 调用,应实现为返回一个可渲染的结果(如 TemplateResult),用于渲染组件的 DOM。

参数无。
更新?否。此方法内的属性变化不会触发元素更新。
需要调用 super?不需要。
在服务器上调用?是。

render() 方法没有参数,但通常它会引用组件的属性。详见渲染

render() {
const header = `<header>${this.header}</header>`;
const content = `<section>${this.content}</section>`;
return html`${header}${content}`;
}

在调用 update() 将更改渲染到组件的 DOM 之后,你可以使用以下方法对组件的 DOM 执行操作。

在组件的 DOM 第一次更新之后、updated() 之前立即调用。

参数changedPropertiesMap,键是已更改属性的名称,值是对应的之前的值。
更新?是。此方法内的属性变化会安排一个新的更新周期。
需要调用 super?不需要。
在服务器上调用?否。

实现 firstUpdated() 在组件的 DOM 创建后执行一次性工作。一些示例可能包括聚焦特定的渲染元素或为元素添加 ResizeObserverIntersectionObserver

firstUpdated() {
this.renderRoot.getElementById('my-text-area').focus();
}

在组件的更新完成且元素的 DOM 已更新和渲染后调用。

参数changedPropertiesMap,键是已更改属性的名称,值是对应的之前的值。
更新?是。此方法内的属性变化会触发元素更新。
需要调用 super?不需要。
在服务器上调用?否。

实现 updated() 来执行在更新后使用元素 DOM 的任务。例如,执行动画的代码可能需要测量元素 DOM。

updated(changedProperties: Map<string, any>) {
if (changedProperties.has('collapsed')) {
this._measureDOM();
}
}
updated(changedProperties) {
if (changedProperties.has('collapsed')) {
this._measureDOM();
}
}

updateComplete Promise 在元素完成更新后解决。使用 updateComplete 来等待更新。解析值是一个布尔值,指示元素是否已完成更新。如果在更新周期完成后没有待处理的更新,它将为 true

当元素更新时,它可能导致其子元素也更新。默认情况下,updateComplete Promise 在元素的更新完成时解决,但不会等待任何子元素完成其更新。此行为可以通过重写 getUpdateComplete 来自定义。

需要知道元素更新何时完成的几种使用场景:

  1. 测试 编写测试时,你可以在对组件的 DOM 进行断言之前 await updateComplete Promise。如果断言依赖于组件整个后代树的更新完成,await requestAnimationFrame 通常是更好的选择,因为 Lit 的默认调度使用浏览器的微任务队列,该队列在动画帧之前被清空。这确保了页面上所有待处理的 Lit 更新在 requestAnimationFrame 回调之前已完成。

  2. 测量 某些组件可能需要测量 DOM 以实现某些布局。虽然始终最好使用纯 CSS 而不是基于 JavaScript 的测量来实现布局,但有时 CSS 的限制使这不可避免。在非常简单的情况下,如果你在测量 Lit 或 ReactiveElement 组件,在状态更改之后、测量之前 await updateComplete 可能就足够了。然而,因为 updateComplete 不会等待所有后代的更新,我们建议使用 ResizeObserver 作为在布局变化时触发测量代码的更可靠方式。

  3. 事件 在渲染完成后从组件派发事件是一个好习惯,这样事件的监听器可以看到组件的完整渲染状态。为此,你可以在触发事件之前 await updateComplete Promise。

    async _loginClickHandler() {
    this.loggedIn = true;
    // 等待 `loggedIn` 状态被渲染到 DOM
    await this.updateComplete;
    this.dispatchEvent(new Event('login'));
    }

updateComplete Promise 在更新周期中有未处理的错误时会被拒绝。更多信息请参阅处理更新周期中的错误

如果你在 render()update() 等生命周期方法中有未捕获的异常,它会导致 updateComplete Promise 被拒绝。 如果你在生命周期方法中有可以抛出异常的代码,最好将其放在 try/catch 语句中。

如果你在 await updateComplete Promise,你也可能想使用 try/catch

try {
await this.updateComplete;
} catch (e) {
/* 处理错误 */
}

在某些情况下,代码可能在意想不到的地方抛出异常。作为后备方案,你可以为 window.onunhandledrejection 添加处理器来捕获这些问题。例如,你可以使用它将错误报告回后端服务,以帮助诊断难以重现的问题。

window.onunhandledrejection = function(e) {
/* 处理错误 */
}

本节介绍一些不常见的用于自定义更新周期的方法。

重写 scheduleUpdate() 来自定义更新的时机。scheduleUpdate() 在即将执行更新时被调用,默认情况下它会立即调用 performUpdate()。重写它可以推迟更新——这种技术可用于解除主渲染/事件线程的阻塞。

例如,以下代码将更新安排在下一帧绘制之后发生,如果更新开销较大,这可以减少卡顿:

protected override async scheduleUpdate(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve));
super.scheduleUpdate();
}
async scheduleUpdate() {
await new Promise((resolve) => setTimeout(resolve));
super.scheduleUpdate();
}

如果你重写了 scheduleUpdate(),你有责任调用 super.scheduleUpdate() 来执行待处理的更新。

async 函数是可选的。

此示例展示了一个 async 函数,它_隐式_返回一个 Promise。你也可以将 scheduleUpdate() 编写为_显式_返回 Promise 的函数。无论哪种情况,下一次更新都要等到 scheduleUpdate() 返回的 Promise 解决后才会开始。

实现响应式更新周期,调用其他方法,如 shouldUpdate()update()updated()

调用 performUpdate() 来立即处理待处理的更新。通常不需要这样做,但在需要同步更新的少数情况下可以这样做。(如果没有待处理的更新,你可以先调用 requestUpdate() 再调用 performUpdate() 来强制同步更新。)

使用 scheduleUpdate() 来自定义调度。

如果你想自定义更新的调度方式,重写 scheduleUpdate()。之前我们建议为此目的重写 performUpdate()。这仍然可行,但它使得调用 performUpdate() 来同步处理待处理的更新变得更加困难。

hasUpdated 属性在组件至少更新过一次时返回 true。你可以在任何生命周期方法中使用 hasUpdated 来仅在组件尚未更新时执行工作。

要在满足 updateComplete Promise 之前等待额外的条件,请重写 getUpdateComplete() 方法。例如,等待子元素的更新可能很有用。首先 await super.getUpdateComplete(),然后等待任何后续状态。

建议重写 getUpdateComplete() 方法而不是 updateComplete getter,以确保与使用 TypeScript ES5 输出的用户兼容(参见 TypeScript#338)。

class MyElement extends LitElement {
async getUpdateComplete() {
await super.getUpdateComplete();
await this._myChild.updateComplete;
}
}

外部生命周期钩子:控制器和装饰器

Permalink to "外部生命周期钩子:控制器和装饰器"

除了组件类实现生命周期回调之外,外部代码(如装饰器)可能需要挂接到组件的生命周期。

Lit 为外部代码与响应式更新生命周期集成提供了两个概念:static addInitializer()addController()

addInitializer() 允许访问 Lit 类定义的代码在类的实例被构造时运行代码。

这在编写自定义装饰器时非常有用。装饰器在类定义时运行,可以做诸如替换字段和方法定义之类的事情。如果它们还需要在实例创建时做额外的工作,它们必须调用 addInitializer()。通常会使用它来添加响应式控制器,以便装饰器可以挂接到组件的生命周期:

// TypeScript 装饰器
const myDecorator = (proto: ReactiveElement, key: string) => {
const ctor = proto.constructor as typeof ReactiveElement;

ctor.addInitializer((instance: ReactiveElement) => {
// 在元素构造期间运行
new MyController(instance);
});
};
// Babel "Stage 2" 装饰器
const myDecorator = (descriptor) => {
...descriptor,
finisher(ctor) {
ctor.addInitializer((instance) => {
// 在元素构造期间运行
new MyController(instance);
});
},
};

装饰一个字段将导致每个实例运行一个初始化器来添加控制器:

class MyElement extends LitElement {
@myDecorator foo;
}

初始化器按构造函数存储。向子类添加初始化器不会添加到超类。由于初始化器在构造函数中运行,初始化器将按照类层次结构的顺序运行,从超类开始,逐步到实例的类。

addController() 将响应式控制器添加到 Lit 组件,使组件调用控制器的生命周期回调。详见响应式控制器文档。

removeController() 移除响应式控制器,使其不再从此组件接收生命周期回调。

Lit 的服务器端渲染包目前正在积极开发中,因此以下信息可能会有所变化。

在服务器上渲染 Lit 时,并非所有更新周期都会被调用。以下方法会在服务器上被调用。