生命周期

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

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

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

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

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

保存已在元素上设置的任何属性。这确保在升级之前设置的值得以维护,并正确地覆盖组件设置的默认值。

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

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

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

一旦元素至少连接到文档一次,组件更新将继续进行,无论元素的连接状态如何。

connectedCallback() 中,您应该设置仅在元素连接到文档时才执行的任务。其中最常见的是向元素外部的节点添加事件监听器,例如添加到窗口的按键事件处理程序。通常,在 connectedCallback() 中完成的任何操作都应该在元素断开连接时撤消 - 例如,从窗口中删除事件监听器,以防止内存泄漏。

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

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

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

**无需删除内部事件监听器。** 您无需删除添加到组件自身 DOM 上的事件监听器 - 包括那些在您的模板中声明性地添加的事件监听器。与外部事件监听器不同,这些不会阻止组件被垃圾回收。

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

Lit 使用此回调将属性中的更改同步到响应式属性。具体来说,当设置属性时,会设置相应的属性。Lit 还自动设置元素的 observedAttributes 数组以匹配组件的响应式属性列表。

您很少需要实现此回调。

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

请注意,adoptedCallback 未被填充。

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

此回调仅应在元素行为应在更改文档时更改的高级用例中使用。

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

当响应式属性发生变化或显式调用 requestUpdate() 方法时,响应式更新周期会被触发。Lit 异步执行更新,因此属性更改会被批量处理 - 如果在请求更新后但在更新开始之前发生了更多属性更改,则所有更改都会在同一个更新中捕获。

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

在高级别上,响应式更新周期是

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

更详细地说,它看起来像这样

更新前

更新

更新后

许多响应式更新方法都接收一个 Map 的更改属性。Map 的键是属性名称,它的值是先前的属性值。您可以始终使用 this.propertythis[property] 找到当前属性值。

如果您使用的是 TypeScript,并且想要对 changedProperties 地图进行严格的类型检查,您可以使用 PropertyValues<this>,它会为每个属性名称推断出正确的类型。

如果您不太关心强类型化 - 或者您只检查属性名称,而不检查先前值 - 您可以使用不太严格的类型,如 Map<string, any>

请注意,PropertyValues<this> 不会识别 protectedprivate 属性。如果您要检查任何 protectedprivate 属性,您需要使用不太严格的类型。

更新期间更改属性(直至且包括 render() 方法)会更新 changedProperties 地图,但不会触发新的更新。在 render() 之后更改属性(例如,在 updated() 方法中)会触发一个新的更新周期,并且更改的属性将被添加到一个新的 changedProperties 地图中,用于下一个周期。

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

当设置响应式属性时调用。默认情况下,hasChanged() 会进行严格的相等性检查,如果它返回 true,则会安排更新。有关更多信息,请参见配置 hasChanged()

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

已更改的属性列表存储在一个 changedProperties 地图中,该地图传递给后续生命周期方法。地图的键是属性名称,它的值是先前属性值。

您可以选择在调用 requestUpdate() 时传递属性名称和先前值,这些值将存储在 changedProperties 地图中。如果您为属性实现了一个自定义 getter 和 setter,这将很有用。有关实现自定义 getter 和 setter 的更多信息,请参见响应式属性

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

任何通常会触发更新的更改,只要组件正在更新,就不会安排新的更新。这样做是为了在更新过程中可以计算属性值。在更新期间更改的属性会反映在changedProperties映射中,因此后续的生命周期方法可以根据这些更改进行操作。

用于确定是否需要更新循环。

参数changedProperties: 一个Map,键是更改的属性名称,值是相应的先前值。
更新否。此方法内部的属性更改不会触发元素更新。
调用超类?不需要。
在服务器上调用?否。

如果shouldUpdate()返回true(默认情况下它会返回true),那么更新将正常进行。如果它返回false,更新循环的其余部分将不会被调用,但updateComplete Promise 仍然会被解决。

您可以实现shouldUpdate()来指定哪些属性更改应该导致更新。使用changedProperties映射来比较当前值和先前值。

update()之前调用,用于计算更新过程中需要的数值。

参数changedProperties: 一个Map,键是更改的属性名称,值是相应的先前值。
更新?否。此方法内部的属性更改不会触发元素更新。
调用超类?不需要。
在服务器上调用?是。

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

用于更新组件的 DOM。

参数changedProperties: 一个Map,键是更改的属性名称,值是相应的先前值。
更新?否。此方法内部的属性更改不会触发元素更新。
调用超类?是。如果没有调用超类,元素的属性和模板将不会更新。
在服务器上调用?否。

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

通常情况下,您不需要实现此方法。

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

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

render()方法没有参数,但通常它会引用组件属性。有关更多信息,请参阅渲染

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

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

参数changedProperties: 一个Map,键是更改的属性名称,值是相应的先前值。
更新?是。此方法内部的属性更改会安排新的更新循环。
调用超类?不需要。
在服务器上调用?否。

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

每当组件的更新完成并且元素的 DOM 已被更新和渲染时调用。

参数changedProperties: 一个Map,键是更改的属性名称,值是相应的先前值。
更新?是。此方法内部的属性更改会触发元素更新。
调用超类?不需要。
在服务器上调用?否。

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

当元素完成更新时,updateComplete Promise 会被解决。使用updateComplete来等待更新。解决的值是一个布尔值,指示元素是否已完成更新。如果更新循环完成后没有待处理的更新,它将为true

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

有几个用例需要知道元素的更新何时完成。

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

  2. 测量 一些组件可能需要测量 DOM 才能实现某些布局。虽然使用纯 CSS 而不是基于 JavaScript 的测量来实现布局总是更好的选择,但有时 CSS 的局限性使得这是不可避免的。在非常简单的情况下,如果您正在测量 Lit 或 ReactiveElement 组件,等待状态更改后以及测量之前等待updateComplete可能就足够了。但是,由于updateComplete不会等待所有后代的更新,因此我们建议使用ResizeObserver作为一种更健壮的方法,在布局发生变化时触发测量代码。

  3. 事件 在渲染完成后从组件中分发事件是一个很好的做法,这样事件的监听器就能看到组件的完整渲染状态。为此,您可以在触发事件之前等待updateComplete Promise。

如果更新循环期间出现未处理的错误,updateComplete Promise 会被拒绝。有关更多信息,请参阅处理更新循环中的错误

如果您在render()update()等生命周期方法中有一个未捕获的异常,它会导致updateComplete Promise 被拒绝。如果您在生命周期方法中有一些可能抛出异常的代码,那么将其放在try/catch语句中是一个好习惯。

如果您正在等待updateComplete Promise,您可能也希望使用try/catch

在某些情况下,代码可能会在意外的地方抛出异常。作为后备措施,您可以添加一个window.onunhandledrejection的处理程序来捕获这些问题。例如,您可以使用它将错误报告回后端服务,以帮助诊断难以重现的问题。

本节介绍了一些用于自定义更新循环的不太常用的方法。

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

例如,以下代码将更新安排在下一帧绘制之后发生,如果更新很昂贵,这可以减少卡顿。

如果您覆盖了scheduleUpdate(),那么您有责任调用super.scheduleUpdate()来执行待处理的更新。

异步函数可选。

此示例显示了一个异步函数,它隐式返回一个 Promise。您也可以将scheduleUpdate()编写为一个显式返回Promise的函数。无论哪种情况,下一个更新都不会开始,直到scheduleUpdate()返回的 Promise 被解决。

实现反应式更新循环,调用其他方法,如shouldUpdate()update()updated()

调用performUpdate()立即处理待处理的更新。这通常是不需要的,但它可以在您需要同步更新的极少数情况下完成。(如果没有任何待处理的更新,您可以调用requestUpdate(),然后调用performUpdate()来强制进行同步更新。)

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

如果您想自定义更新的调度方式,请覆盖scheduleUpdate()。以前,我们建议为此目的覆盖performUpdate()。这仍然有效,但它使同步处理待处理更新更难调用performUpdate()

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

为了在完成updateComplete Promise 之前等待额外的条件,请覆盖getUpdateComplete()方法。例如,等待子元素的更新可能很有用。首先等待super.getUpdateComplete(),然后等待任何后续状态。

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

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

“外部生命周期钩子:控制器和装饰器”的永久链接

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

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

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

当编写自定义装饰器时,这非常有用。装饰器在类定义时运行,可以执行替换字段和方法定义之类的操作。如果它们还需要在实例创建时执行工作,则必须调用addInitializer()。这通常用于添加一个反应式控制器,这样装饰器就可以钩入组件生命周期。

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

初始化器是针对每个构造函数存储的。向子类添加初始化器不会将其添加到超类。由于初始化器在构造函数中运行,因此初始化器将按照类层次结构的顺序运行,从超类开始,一直到实例的类。

addController()向 Lit 组件添加一个反应式控制器,以便组件调用控制器的生命周期回调。有关更多信息,请参阅反应式控制器文档。

removeController()删除一个反应式控制器,以便它不再接收来自此组件的生命周期回调。

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

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