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

更新

更新后

changedProperties 地图
“changedProperties 地图”的永久链接许多响应式更新方法都接收一个 Map
的更改属性。Map
的键是属性名称,它的值是先前的属性值。您可以始终使用 this.property
或 this[property]
找到当前属性值。
changedProperties 的 TypeScript 类型
“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
属性,您需要使用不太严格的类型。
在更新期间更改属性
“在更新期间更改属性”的永久链接在更新期间更改属性(直至且包括 render()
方法)会更新 changedProperties
地图,但不会触发新的更新。在 render()
之后更改属性(例如,在 updated()
方法中)会触发一个新的更新周期,并且更改的属性将被添加到一个新的 changedProperties
地图中,用于下一个周期。
触发更新
“触发更新”的永久链接当响应式属性发生变化或调用 requestUpdate()
方法时,会触发更新。由于更新是异步执行的,因此在执行更新之前发生的所有更改只会导致一次更新。
hasChanged()
“hasChanged()”的永久链接当设置响应式属性时调用。默认情况下,hasChanged()
会进行严格的相等性检查,如果它返回 true
,则会安排更新。有关更多信息,请参见配置 hasChanged()
。
requestUpdate()
“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()
“shouldUpdate()”的永久链接用于确定是否需要更新循环。
参数 | changedProperties : 一个Map ,键是更改的属性名称,值是相应的先前值。 |
更新 | 否。此方法内部的属性更改不会触发元素更新。 |
调用超类? | 不需要。 |
在服务器上调用? | 否。 |
如果shouldUpdate()
返回true
(默认情况下它会返回true
),那么更新将正常进行。如果它返回false
,更新循环的其余部分将不会被调用,但updateComplete
Promise 仍然会被解决。
您可以实现shouldUpdate()
来指定哪些属性更改应该导致更新。使用changedProperties
映射来比较当前值和先前值。
shouldUpdate(changedProperties: Map<string, any>) {
// Only update element if prop1 changed.
return changedProperties.has('prop1');
}
shouldUpdate(changedProperties) {
// Only update element if prop1 changed.
return changedProperties.has('prop1');
}
willUpdate()
“willUpdate()”的永久链接在update()
之前调用,用于计算更新过程中需要的数值。
参数 | changedProperties : 一个Map ,键是更改的属性名称,值是相应的先前值。 |
更新? | 否。此方法内部的属性更改不会触发元素更新。 |
调用超类? | 不需要。 |
在服务器上调用? | 是。 |
实现willUpdate()
来计算依赖于其他属性并在更新过程的其余部分中使用的属性值。
willUpdate(changedProperties: PropertyValues<this>) {
// only need to check changed properties for an expensive computation.
if (changedProperties.has('firstName') || changedProperties.has('lastName')) {
this.sha = computeSHA(`${this.firstName} ${this.lastName}`);
}
}
render() {
return html`SHA: ${this.sha}`;
}
willUpdate(changedProperties) {
// only need to check changed properties for an expensive computation.
if (changedProperties.has('firstName') || changedProperties.has('lastName')) {
this.sha = computeSHA(`${this.firstName} ${this.lastName}`);
}
}
render() {
return html`SHA: ${this.sha}`;
}
update()
“update()”的永久链接用于更新组件的 DOM。
参数 | changedProperties : 一个Map ,键是更改的属性名称,值是相应的先前值。 |
更新? | 否。此方法内部的属性更改不会触发元素更新。 |
调用超类? | 是。如果没有调用超类,元素的属性和模板将不会更新。 |
在服务器上调用? | 否。 |
将属性值反映到属性中,并调用render()
来更新组件的内部 DOM。
通常情况下,您不需要实现此方法。
render()
“render()”的永久链接由update()
调用,应实现为返回一个可渲染的结果(例如TemplateResult
),用于渲染组件的 DOM。
参数 | 无。 |
更新? | 否。此方法内部的属性更改不会触发元素更新。 |
调用超类? | 不需要。 |
在服务器上调用? | 是。 |
render()
方法没有参数,但通常它会引用组件属性。有关更多信息,请参阅渲染。
render() {
const header = `<header>${this.header}</header>`;
const content = `<section>${this.content}</section>`;
return html`${header}${content}`;
}
完成更新
“完成更新”的永久链接在调用update()
来渲染对组件 DOM 的更改后,您可以使用以下方法对组件的 DOM 执行操作。
firstUpdated()
“firstUpdated()”的永久链接在组件的 DOM 第一次被更新后立即调用,在updated()
被调用之前。
参数 | changedProperties : 一个Map ,键是更改的属性名称,值是相应的先前值。 |
更新? | 是。此方法内部的属性更改会安排新的更新循环。 |
调用超类? | 不需要。 |
在服务器上调用? | 否。 |
实现firstUpdated()
来在组件的 DOM 被创建后执行一次性工作。一些示例可能包括聚焦特定的渲染元素,或者向元素添加ResizeObserver或IntersectionObserver。
firstUpdated() {
this.renderRoot.getElementById('my-text-area').focus();
}
updated()
“updated()”的永久链接每当组件的更新完成并且元素的 DOM 已被更新和渲染时调用。
参数 | changedProperties : 一个Map ,键是更改的属性名称,值是相应的先前值。 |
更新? | 是。此方法内部的属性更改会触发元素更新。 |
调用超类? | 不需要。 |
在服务器上调用? | 否。 |
实现updated()
来执行在更新后使用元素 DOM 的任务。例如,执行动画的代码可能需要测量元素 DOM。
updated(changedProperties: Map<string, any>) {
if (changedProperties.has('collapsed')) {
this._measureDOM();
}
}
updated(changedProperties) {
if (changedProperties.has('collapsed')) {
this._measureDOM();
}
}
updateComplete
“updateComplete”的永久链接当元素完成更新时,updateComplete
Promise 会被解决。使用updateComplete
来等待更新。解决的值是一个布尔值,指示元素是否已完成更新。如果更新循环完成后没有待处理的更新,它将为true
。
当元素更新时,它也可能导致其子元素更新。默认情况下,updateComplete
Promise 在元素的更新完成后被解决,但不会等待任何子元素完成其更新。此行为可以通过覆盖getUpdateComplete
来自定义。
有几个用例需要知道元素的更新何时完成。
测试 在编写测试时,您可以在对组件的 DOM 进行断言之前,等待
updateComplete
Promise。如果断言依赖于组件的整个后代树的更新完成,那么等待requestAnimationFrame
通常是更好的选择,因为 Lit 的默认调度使用浏览器的微任务队列,该队列在动画帧之前被清空。这确保了在requestAnimationFrame
回调之前,页面上所有待处理的 Lit 更新都已完成。测量 一些组件可能需要测量 DOM 才能实现某些布局。虽然使用纯 CSS 而不是基于 JavaScript 的测量来实现布局总是更好的选择,但有时 CSS 的局限性使得这是不可避免的。在非常简单的情况下,如果您正在测量 Lit 或 ReactiveElement 组件,等待状态更改后以及测量之前等待
updateComplete
可能就足够了。但是,由于updateComplete
不会等待所有后代的更新,因此我们建议使用ResizeObserver
作为一种更健壮的方法,在布局发生变化时触发测量代码。事件 在渲染完成后从组件中分发事件是一个很好的做法,这样事件的监听器就能看到组件的完整渲染状态。为此,您可以在触发事件之前等待
updateComplete
Promise。async _loginClickHandler() {
this.loggedIn = true;
// Wait for `loggedIn` state to be rendered to the DOM
await this.updateComplete;
this.dispatchEvent(new Event('login'));
}
如果更新循环期间出现未处理的错误,updateComplete
Promise 会被拒绝。有关更多信息,请参阅处理更新循环中的错误。
处理更新周期中的错误
“处理更新循环中的错误”的永久链接如果您在render()
或update()
等生命周期方法中有一个未捕获的异常,它会导致updateComplete
Promise 被拒绝。如果您在生命周期方法中有一些可能抛出异常的代码,那么将其放在try
/catch
语句中是一个好习惯。
如果您正在等待updateComplete
Promise,您可能也希望使用try
/catch
。
try {
await this.updateComplete;
} catch (e) {
/* handle error */
}
在某些情况下,代码可能会在意外的地方抛出异常。作为后备措施,您可以添加一个window.onunhandledrejection
的处理程序来捕获这些问题。例如,您可以使用它将错误报告回后端服务,以帮助诊断难以重现的问题。
window.onunhandledrejection = function(e) {
/* handle error */
}
实施额外的定制
“实现额外的自定义”的永久链接本节介绍了一些用于自定义更新循环的不太常用的方法。
scheduleUpdate()
“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()
“performUpdate() ”的永久链接实现反应式更新循环,调用其他方法,如shouldUpdate()
、update()
和updated()
。
调用performUpdate()
立即处理待处理的更新。这通常是不需要的,但它可以在您需要同步更新的极少数情况下完成。(如果没有任何待处理的更新,您可以调用requestUpdate()
,然后调用performUpdate()
来强制进行同步更新。)
使用scheduleUpdate()
来自定义调度。
如果您想自定义更新的调度方式,请覆盖scheduleUpdate()
。以前,我们建议为此目的覆盖performUpdate()
。这仍然有效,但它使同步处理待处理更新更难调用performUpdate()
。
hasUpdated
“hasUpdated ”的永久链接如果组件至少更新过一次,则hasUpdated
属性将返回 true。您可以在任何生命周期方法中使用hasUpdated
来仅在组件尚未更新时执行工作。
getUpdateComplete()
“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;
}
}
外部生命周期钩子:控制器和装饰器
“外部生命周期钩子:控制器和装饰器”的永久链接除了组件类实现生命周期回调之外,外部代码(例如装饰器)可能需要钩入组件的生命周期。
Lit 提供了两种概念让外部代码与反应式更新生命周期集成:static addInitializer()
和 addController()
static addInitializer()
“static addInitializer()”的永久链接addInitializer()
允许访问 Lit 类定义的代码在类的实例被构造时运行代码。
当编写自定义装饰器时,这非常有用。装饰器在类定义时运行,可以执行替换字段和方法定义之类的操作。如果它们还需要在实例创建时执行工作,则必须调用addInitializer()
。这通常用于添加一个反应式控制器,这样装饰器就可以钩入组件生命周期。
// A TypeScript decorator
const myDecorator = (proto: ReactiveElement, key: string) => {
const ctor = proto.constructor as typeof ReactiveElement;
ctor.addInitializer((instance: ReactiveElement) => {
// This is run during construction of the element
new MyController(instance);
});
};
// A Babel "Stage 2" decorator
const myDecorator = (descriptor) => {
...descriptor,
finisher(ctor) {
ctor.addInitializer((instance) => {
// This is run during construction of the element
new MyController(instance);
});
},
};
装饰一个字段将导致每个实例运行一个添加控制器的初始化器。
class MyElement extends LitElement {
@myDecorator foo;
}
初始化器是针对每个构造函数存储的。向子类添加初始化器不会将其添加到超类。由于初始化器在构造函数中运行,因此初始化器将按照类层次结构的顺序运行,从超类开始,一直到实例的类。
addController()
“addController()”的永久链接addController()
向 Lit 组件添加一个反应式控制器,以便组件调用控制器的生命周期回调。有关更多信息,请参阅反应式控制器文档。
removeController()
“removeController()”的永久链接removeController()
删除一个反应式控制器,以便它不再接收来自此组件的生命周期回调。
服务器端响应式更新周期
“服务器端反应式更新循环”的永久链接Lit 的服务器端渲染包目前正在积极开发中,因此以下信息可能会发生变化。
在服务器上渲染 Lit 时,并非所有更新循环都会被调用。以下方法是在服务器上调用的。
