生命周期
Lit 组件使用标准的自定义元素生命周期方法。此外,Lit 引入了一个响应式更新周期,当响应式属性发生变化时,它会渲染 DOM 的变化。
标准自定义元素生命周期
Permalink to "标准自定义元素生命周期"Lit 组件是标准的自定义元素,并继承了自定义元素的生命周期方法。有关自定义元素生命周期的信息,请参阅 MDN 上的使用生命周期回调。
如果你需要自定义任何标准自定义元素生命周期方法,请确保调用 super
实现(例如 super.connectedCallback()
),以便保持标准的 Lit 功能。
constructor()
Permalink to "constructor()"在创建元素时调用。另外,当现有元素升级时也会调用它,这种情况发生在自定义元素的定义在元素已经在 DOM 中后才加载时。
Lit 行为
Permalink to "Lit 行为"使用 requestUpdate()
方法请求异步更新,因此当 Lit 组件升级时,它会立即执行更新。
保存已经设置在元素上的任何属性。这确保了在升级前设置的值得到保持,并正确覆盖组件设置的默认值。
执行必须在第一次更新之前完成的一次性初始化任务。例如,当不使用装饰器时,可以在构造函数中设置属性的默认值,如在静态属性字段中声明属性所示。
constructor() {
super();
this.foo = 'foo';
this.bar = 'bar';
}
connectedCallback()
Permalink to "connectedCallback()"当组件添加到文档的 DOM 中时调用。
Lit 行为
Permalink to "Lit 行为"Lit 在元素连接后启动第一个元素更新周期。在准备渲染时,Lit 还确保创建 renderRoot
(通常是它的 shadowRoot
)。
一旦元素至少连接到文档一次,组件更新将继续进行,无论元素的连接状态如何。
在 connectedCallback()
中,你应该设置只在元素连接到文档时才应发生的任务。最常见的是向元素外部的节点添加事件监听器,例如向窗口添加按键事件处理器。通常,在 connectedCallback()
中做的任何事情都应该在元素断开连接时撤销 — 例如,移除窗口上的事件监听器以防止内存泄漏。
connectedCallback() {
super.connectedCallback()
window.addEventListener('keydown', this._handleKeydown);
}
disconnectedCallback()
Permalink to "disconnectedCallback()"当组件从文档的 DOM 中移除时调用。
Lit 行为
Permalink to "Lit 行为"暂停响应式更新周期。当元素连接时会恢复。
这个回调是向元素发出的主要信号,表明它可能不再被使用;因此,disconnectedCallback()
应确保没有任何东西持有元素的引用(例如添加到元素外部节点的事件监听器),以便它可以被垃圾回收。因为元素在断开连接后可能会重新连接,例如在 DOM 中移动元素或缓存的情况下,所以可能需要通过 connectedCallback()
重新建立任何此类引用或监听器,以便元素在这些场景中继续按预期工作。例如,移除添加到窗口的按键事件处理器等元素外部节点的事件监听器。
disconnectedCallback() {
super.disconnectedCallback()
window.removeEventListener('keydown', this._handleKeydown);
}
无需移除内部事件监听器。 你不需要移除在组件自己的 DOM 上添加的事件监听器——包括在模板中以声明方式添加的监听器。与外部事件监听器不同,这些不会阻止组件被垃圾回收。
attributeChangedCallback()
Permalink to "attributeChangedCallback()"当元素的 observedAttributes
之一发生变化时调用。
Lit 行为
Permalink to "Lit 行为"Lit 使用此回调将属性中的变化同步到响应式属性。具体来说,当设置属性时,相应的属性也会被设置。Lit 还会自动设置元素的 observedAttributes
数组,使其与组件的响应式属性列表匹配。
你很少需要实现这个回调。
adoptedCallback()
Permalink to "adoptedCallback()"当一个组件被移动到新文档时调用。
请注意,adoptedCallback
没有 polyfill 支持。
Lit 行为
Permalink to "Lit 行为"Lit 对此回调没有默认行为。
此回调只应用于当元素更改文档时其行为应该改变的高级用例。
响应式更新周期
Permalink to "响应式更新周期"除了标准的自定义元素生命周期外,Lit 组件还实现了响应式更新周期。
响应式更新周期在响应式属性更改或显式调用 requestUpdate()
方法时触发。Lit 异步执行更新,因此属性更改会被批处理 — 如果在请求更新后但在更新开始前有更多属性变化,所有的变化都会被捕获在同一次更新中。
更新发生在微任务时机,这意味着它们在浏览器绘制下一帧到屏幕之前发生。有关浏览器时间的更多信息,请参阅 Jake Archibald 关于微任务的文章。
在高层次上,响应式更新周期是:
- 当一个或多个属性更改或调用
requestUpdate()
时,会安排一次更新。 - 更新在下一帧绘制之前执行。
- 设置反射属性。
- 调用组件的渲染方法来更新其内部 DOM。
- 更新完成,
updateComplete
承诺得到解决。
更详细地说,它看起来像这样:
更新前

更新

更新后

changedProperties 映射
Permalink to "changedProperties 映射"许多响应式更新方法接收一个已更改属性的 Map
。Map
的键是属性名,其值是先前的属性值。你始终可以使用 this.property
或 this[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>
不识别 protected
或 private
属性。如果你检查任何 protected
或 private
属性,你需要使用限制性较少的类型。
在更新期间更改属性
Permalink to "在更新期间更改属性"在更新期间(直到并包括 render()
方法)更改属性会更新 changedProperties
映射,但不会触发新的更新。在 render()
之后(例如,在 updated()
方法中)更改属性会触发新的更新周期,并且更改的属性会添加到一个新的 changedProperties
映射中,用于下一个周期。
当响应式属性更改或调用 requestUpdate()
方法时,会触发更新。由于更新是异步执行的,在执行更新之前发生的任何和所有更改仅导致单次更新。
hasChanged()
Permalink to "hasChanged()"当设置响应式属性时调用。默认情况下,hasChanged()
执行严格相等检查,如果返回 true
,则安排更新。有关更多信息,请参阅配置 hasChanged()
。
requestUpdate()
Permalink to "requestUpdate()"调用 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
映射中,因此后续的生命周期方法可以处理这些更改。
shouldUpdate()
Permalink to "shouldUpdate()"调用以确定是否需要更新周期。
参数 | changedProperties :Map ,键是已更改属性的名称,值是相应的先前值。 |
更新 | 否。此方法中的属性更改不会触发元素更新。 |
调用 super? | 不必要。 |
在服务器上调用? | 否。 |
如果 shouldUpdate()
返回 true
(默认情况下会返回 true
),那么更新将正常进行。如果返回 false
,则不会调用更新周期的其余部分,但 updateComplete
Promise 仍会得到解决。
你可以实现 shouldUpdate()
来指定哪些属性更改应该导致更新。使用 changedProperties
映射来比较当前值和先前值。
shouldUpdate(changedProperties: Map<string, any>) {
// 只在 prop1 更改时更新元素。
return changedProperties.has('prop1');
}
shouldUpdate(changedProperties) {
// 只在 prop1 更改时更新元素。
return changedProperties.has('prop1');
}
willUpdate()
Permalink to "willUpdate()"在 update()
之前调用,以计算更新期间需要的值。
参数 | changedProperties :Map ,键是已更改属性的名称,值是相应的先前值。 |
更新? | 否。此方法中的属性更改不会触发元素更新。 |
调用 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}`;
}
update()
Permalink to "update()"调用以更新组件的 DOM。
参数 | changedProperties :Map ,键是已更改属性的名称,值是相应的先前值。 |
更新? | 否。此方法中的属性更改不会触发元素更新。 |
调用 super? | 是。没有 super 调用,元素的属性和模板将不会更新。 |
在服务器上调用? | 否。 |
将属性值反映到属性并调用 render()
以更新组件的内部 DOM。
通常,你不需要实现此方法。
render()
Permalink to "render()"由 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 执行操作。
firstUpdated()
Permalink to "firstUpdated()"在组件的 DOM 首次更新后、在调用 updated()
之前立即调用。
参数 | changedProperties :Map ,键是已更改属性的名称,值是相应的先前值。 |
更新? | 是。此方法中的属性更改会安排新的更新周期。 |
调用 super? | 不必要。 |
在服务器上调用? | 否。 |
实现 firstUpdated()
来在组件的 DOM 被创建后执行一次性工作。一些例子可能包括聚焦特定的渲染元素或向元素添加 ResizeObserver 或 IntersectionObserver。
firstUpdated() {
this.renderRoot.getElementById('my-text-area').focus();
}
updated()
Permalink to "updated()"在组件的更新完成和元素的 DOM 已更新和渲染后调用。
参数 | changedProperties :Map ,键是已更改属性的名称,值是相应的先前值。 |
更新? | 是。此方法中的属性更改会触发元素更新。 |
调用 super? | 不必要。 |
在服务器上调用? | 否。 |
实现 updated()
来在更新后执行使用元素 DOM 的任务。例如,执行动画的代码可能需要测量元素 DOM。
updated(changedProperties: Map<string, any>) {
if (changedProperties.has('collapsed')) {
this._measureDOM();
}
}
updated(changedProperties) {
if (changedProperties.has('collapsed')) {
this._measureDOM();
}
}
updateComplete
Permalink to "updateComplete"updateComplete
promise 在元素完成更新时解决。使用 updateComplete
等待更新。解决的值是一个布尔值,表示元素是否已完成更新。如果在更新周期完成后没有待处理的更新,它将为 true
。
当一个元素更新时,它可能会导致其子元素也更新。默认情况下,updateComplete
promise 在元素的更新完成时解决,但不等待任何子元素完成更新。可以通过重写 getUpdateComplete
来自定义此行为。
需要知道元素的更新何时完成有几个用例:
测试 编写测试时,你可以在对组件的 DOM 进行断言之前等待
updateComplete
promise。如果断言依赖于组件整个后代树完成更新,那么等待requestAnimationFrame
通常是更好的选择,因为 Lit 的默认调度使用浏览器的微任务队列,该队列在动画帧之前被清空。这确保了页面上所有待处理的 Lit 更新在调用requestAnimationFrame
回调之前都已完成。测量 一些组件可能需要测量 DOM 以实现某些布局。虽然最好使用纯 CSS 而不是基于 JavaScript 的测量来实现布局,但有时由于 CSS 的限制,这是不可避免的。在非常简单的情况下,如果你正在测量 Lit 或 ReactiveElement 组件,可能只需要在状态更改后和测量之前等待
updateComplete
就足够了。但是,因为updateComplete
不等待所有后代的更新,我们建议使用ResizeObserver
作为一种更健壮的方式,在布局更改时触发测量代码。事件 在渲染完成后从组件分派事件是一个好习惯,这样事件的监听器可以看到组件的完全渲染状态。为此,你可以在触发事件之前等待
updateComplete
promise。async _loginClickHandler() {
this.loggedIn = true;
// 等待 `loggedIn` 状态被渲染到 DOM
await this.updateComplete;
this.dispatchEvent(new Event('login'));
}
如果在更新周期中有未处理的错误,updateComplete
promise 会拒绝。有关更多信息,请参阅处理更新周期中的错误。
处理更新周期中的错误
Permalink to "处理更新周期中的错误"如果你在生命周期方法(如 render()
或 update()
)中有未捕获的异常,它会导致 updateComplete
promise 被拒绝。 如果你在生命周期方法中有可能抛出异常的代码,最好将其放在 try
/catch
语句中。
如果你正在等待 updateComplete
promise,你可能还想使用 try
/catch
:
try {
await this.updateComplete;
} catch (e) {
/* 处理错误 */
}
在某些情况下,代码可能在意外的地方抛出。作为后备,你可以添加 window.onunhandledrejection
的处理程序来捕获这些问题。例如,你可以使用它将错误报告回后端服务,以帮助诊断难以重现的问题。
window.onunhandledrejection = function(e) {
/* 处理错误 */
}
实现额外的自定义
Permalink to "实现额外的自定义"本节介绍一些不太常见的自定义更新周期的方法。
scheduleUpdate()
Permalink to "scheduleUpdate()"重写 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()
来执行待处理的更新。
异步函数可选。
这个例子展示了一个异步函数,它_隐式_返回一个 promise。你也可以将 scheduleUpdate()
写成_显式_返回 Promise
的函数。在任何情况下,下一个更新直到 scheduleUpdate()
返回的 promise 解决后才开始。
performUpdate()
Permalink to "performUpdate()"实现响应式更新周期,调用其他方法,如 shouldUpdate()
、update()
和 updated()
。
调用 performUpdate()
可以立即处理待处理的更新。通常不需要这样做,但在你需要同步更新的罕见情况下可以这样做。(如果没有待处理的更新,你可以调用 requestUpdate()
后跟 performUpdate()
来强制同步更新。)
使用 scheduleUpdate()
自定义调度。
如果你想自定义更新的调度方式,请重写 scheduleUpdate()
。之前,我们建议为此目的重写 performUpdate()
。这仍然有效,但它使得调用 performUpdate()
来同步处理待处理的更新变得更加困难。
hasUpdated
Permalink to "hasUpdated"hasUpdated
属性在组件至少更新一次后返回 true。你可以在任何生命周期方法中使用 hasUpdated
来仅在组件尚未更新时执行工作。
getUpdateComplete()
Permalink to "getUpdateComplete()"要在满足 updateComplete
promise 之前等待额外条件,请重写 getUpdateComplete()
方法。例如,等待子元素的更新可能很有用。首先等待 super.getUpdateComplete()
,然后等待任何后续状态。
建议重写 getUpdateComplete()
方法而不是 updateComplete
getter,以确保与使用 TypeScript 的 ES5 输出的用户兼容(参见 TypeScript#338)。
class MyElement extends LitElement {
async getUpdateComplete() {
const result = await super.getUpdateComplete();
await this._myChild.updateComplete;
return result;
}
}
外部生命周期钩子:控制器和装饰器
Permalink to "外部生命周期钩子:控制器和装饰器"除了组件类实现生命周期回调外,外部代码(如装饰器)可能需要挂钩到组件的生命周期。
Lit 提供了两个概念让外部代码与响应式更新生命周期集成:static addInitializer()
和 addController()
:
static addInitializer()
Permalink to "static addInitializer()"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()
Permalink to "addController()"addController()
向 Lit 组件添加一个响应式控制器,使组件调用控制器的生命周期回调。有关更多信息,请参阅响应式控制器文档。
removeController()
Permalink to "removeController()"removeController()
移除响应式控制器,使其不再从该组件接收生命周期回调。
服务器端响应式更新周期
Permalink to "服务器端响应式更新周期"Lit 的 服务器端渲染包 目前正在积极开发中,因此以下信息可能会发生变化。
在服务器上渲染 Lit 时,不会调用所有的更新周期。以下方法在服务器上被调用。
