Жизненный цикл
Обзор
Компоненты на основе LitElement обновляются асинхронно в ответ на изменения наблюдаемых свойств. Изменения свойств объединяются в пакет: если после запроса обновления, но до его начала, изменятся другие свойства, все изменения будут учтены в одном обновлении.
В общих чертах жизненный цикл обновления выглядит так:
- Устанавливается свойство.
- Проверяется, требуется ли обновление. Если требуется, оно запрашивается.
- Выполняется обновление:
- Обрабатываются свойства и атрибуты.
- Выполняется рендеринг элемента.
- Разрешается Promise, указывающий на завершение обновления.
LitElement и цикл событий браузера
Браузер выполняет код JavaScript, обрабатывая очередь задач в цикле событий. На каждой итерации цикла событий браузер берёт задачу из очереди и выполняет её до завершения.
После завершения задачи, прежде чем взять следующую задачу из очереди, браузер выделяет время на выполнение работы из других источников, включая обновления DOM, взаимодействие с пользователем и очередь микрозадач.
По умолчанию обновления LitElement запрашиваются асинхронно и ставятся в очередь как микрозадачи. Это означает, что шаг 3 выше (выполнение обновления) выполняется в конце следующей итерации цикла событий.
Можно изменить это поведение так, чтобы перед выполнением обновления на шаге 3 ожидался Promise. Подробнее см. в разделе performUpdate.
Более подробное объяснение цикла событий браузера см. в статье Джейка Арчибальда.
Обратные вызовы жизненного цикла
LitElement также наследует стандартные обратные вызовы жизненного цикла из стандарта веб-компонентов:
-
connectedCallback: вызывается, когда компонент добавляется в DOM документа. -
disconnectedCallback: вызывается, когда компонент удаляется из DOM документа. -
adoptedCallback: вызывается, когда компонент перемещается в новый документ. -
attributeChangedCallback: вызывается при изменении атрибута компонента.
Обратите внимание: для adoptedCallback полифил не предоставляется.
Во всех методах жизненного цикла необходимо вызывать метод super.
Пример:
connectedCallback() {
super.connectedCallback()
console.log('connected')
}
Promise и асинхронные функции
LitElement использует объекты Promise для планирования обновлений элементов и реагирования на них.
С помощью async и await удобно работать с Promise. Например, можно дождаться Promise updateComplete:
// `async` makes the function return a Promise & lets you use `await`
async myFunc(data) {
// Set a property, triggering an update
this.myProp = data;
// Wait for the updateComplete promise to resolve
await this.updateComplete;
// ...do stuff...
return 'done';
}
Поскольку функции async возвращают Promise, их также можно ожидать:
let result = await myFunc('stuff');
// `result` is resolved! You can do something with it
Более подробное руководство см. во вводном руководстве Web Fundamentals по Promise.
Различные сценарии использования
Чаще всего к жизненному циклу пользовательского элемента или циклу обновления LitElement подключаются для инициализации, управления производными данными и обработки событий, возникающих вне шаблона элемента. В следующем списке приведены распространённые сценарии использования и способы их решения. В некоторых случаях достичь определённой цели можно несколькими способами. Если изучить этот список вместе с подробным техническим справочником, можно получить достаточно полное представление и выбрать наиболее подходящий вариант для нужд своего компонента.
- Используйте property.hasChanged, чтобы проверить: «Это изменение? Нужно ли запускать цикл обновления?».
- Используйте конструктор элемента для инициализации свойств LitElement со значениями по умолчанию. (Значения атрибутов из DOM недоступны во время выполнения конструктора.)
- Используйте firstUpdated для инициализации приватных полей значениями атрибутов DOM (поскольку конструктор не имеет к ним доступа). Обратите внимание: к этому моменту render уже был вызван, и ваши изменения могут запустить ещё один цикл обновления. Если вам обязательно нужен доступ к значениям атрибутов до первого рендеринга, рассмотрите возможность использования connectedCallback, но дополнительную логику для определения «первого» обновления придётся реализовать самостоятельно, так как connectedCallback может вызываться несколько раз.
- Используйте updated, чтобы поддерживать актуальность производных данных или реагировать на изменения. Если это приводит к повторному рендерингу, рассмотрите возможность использования update.
- Используйте пользовательские геттеры свойств JS для производных данных, которые несложно вычислить, если они вряд ли будут часто меняться и ваш элемент нечасто выполняет повторный рендеринг.
- Используйте requestUpdate, чтобы запустить цикл обновления, если LitElement не может обнаружить изменение самостоятельно. (Например, если у вас есть наблюдаемое свойство — массив, и вы добавляете в этот массив элемент, а не заменяете весь массив, LitElement не «увидит» это изменение, потому что ссылка на массив не изменилась.)
- Используйте connectedCallback, чтобы регистрировать обработчики событий для событий вне шаблона элемента, но не забудьте удалить их в disconnectedCallback!
Справочник по методам и свойствам
Методы и свойства цикла обновления в порядке вызова:
- someProperty.hasChanged
- requestUpdate
- performUpdate
- shouldUpdate
- update
- render
- firstUpdated
- updated
- updateComplete
someProperty.hasChanged
У всех объявленных свойств есть функция hasChanged, которая вызывается при каждой установке свойства; если hasChanged возвращает true, планируется обновление.
Информацию о том, как настроить hasChanged и определить, что считать изменением свойства, см. в документации по свойствам.
requestUpdate
// Manually start an update this.requestUpdate(); // Call from within a custom property setter this.requestUpdate(propertyName, oldValue);
|
Параметры |
propertyNameoldValue
|
Имя свойства, которое нужно обновить. Предыдущее значение свойства. |
| Возвращает | Promise |
Возвращает Promise updateComplete, который разрешается по завершении обновления. |
| Запускает обновление? | Нет | Изменения свойств внутри этого метода не запускают обновление элемента. |
Если hasChanged вернул true, вызывается requestUpdate, и обновление продолжается.
Чтобы вручную запустить обновление элемента, вызовите requestUpdate без параметров.
Чтобы реализовать пользовательский метод установки свойства, поддерживающий параметры свойств, передайте имя свойства и его предыдущее значение в качестве параметров.
Пример: вручную запустить обновление элемента
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
constructor() {
super();
// Request an update in response to an event
this.addEventListener('load-complete', async (e) => {
console.log(e.detail.message);
console.log(await this.requestUpdate());
});
}
render() {
return html`
<button @click="${this.fire}">Fire a "load-complete" event</button>
`;
}
fire() {
let newMessage = new CustomEvent('load-complete', {
detail: { message: 'hello. a load-complete happened.' }
});
this.dispatchEvent(newMessage);
}
}
customElements.define('my-element', MyElement);
Пример: вызвать requestUpdate из пользовательского метода установки свойства
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return { prop: { type: Number } };
}
set prop(val) {
let oldVal = this._prop;
this._prop = Math.floor(val);
this.requestUpdate('prop', oldVal);
}
get prop() { return this._prop; }
constructor() {
super();
this._prop = 0;
}
render() {
return html`
<p>prop: ${this.prop}</p>
<button @click="${() => { this.prop = Math.random()*10; }}">
change prop
</button>
`;
}
}
customElements.define('my-element', MyElement);
performUpdate
/**
* Implement to override default behavior.
*/
performUpdate() { ... }
| Возвращает |
void или Promise
|
Выполняет обновление. |
| Запускает обновление? | Нет | Изменения свойств внутри этого метода не запускают обновление элемента. |
При выполнении обновления вызывается метод performUpdate(). Этот метод вызывает ряд других методов жизненного цикла.
Изменения, которые обычно запускают обновление и происходят во время обновления компонента, не планируют новое обновление. Это сделано для того, чтобы значения свойств можно было вычислять в процессе обновления. Свойства, изменённые во время обновления, отражаются в карте changedProperties, поэтому последующие методы жизненного цикла могут учитывать эти изменения.
По умолчанию performUpdate планируется как микрозадача после завершения следующего выполнения цикла событий браузера. Чтобы запланировать performUpdate, реализуйте его как асинхронный метод, который ожидает определённого состояния, а затем вызывает super.performUpdate(). Например:
async performUpdate() {
await new Promise((resolve) => requestAnimationFrame(() => resolve()));
super.performUpdate();
}
shouldUpdate
/**
* Implement to override default behavior.
*/
shouldUpdate(changedProperties) { ... }
| Параметры | changedProperties |
Map. Ключи — имена изменившихся свойств; значения — соответствующие предыдущие значения. |
| Возвращает | Boolean |
Если true, обновление продолжается. Значение, возвращаемое по умолчанию, — true. |
| Запускает обновление? | Нет | Изменения свойств внутри этого метода не запускают обновление элемента. |
Определяет, должно ли выполняться обновление. Реализуйте shouldUpdate, чтобы указать, какие изменения свойств должны вызывать обновления. По умолчанию этот метод всегда возвращает true.
Пример: настроить, какие изменения свойств должны вызывать обновления
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
prop1: { type: Number },
prop2: { type: Number }
};
}
constructor() {
super();
this.prop1 = 0;
this.prop2 = 0;
}
render() {
return html`
<p>prop1: ${this.prop1}</p>
<p>prop2: ${this.prop2}</p>
<button @click="${() => this.prop1=this.change()}">Change prop1</button>
<button @click="${() => this.prop2=this.change()}">Change prop2</button>
`;
}
/**
* Only update element if prop1 changed.
*/
shouldUpdate(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
console.log(`${propName} changed. oldValue: ${oldValue}`);
});
return changedProperties.has('prop1');
}
change() {
return Math.floor(Math.random()*10);
}
}
customElements.define('my-element', MyElement);
update
| Параметры | changedProperties |
Map. Ключи — имена изменившихся свойств; значения — соответствующие предыдущие значения. |
| Запускает обновление? | Нет | Изменения свойств внутри этого метода не запускают обновление элемента. |
Отражает значения свойств в атрибутах и вызывает render для рендеринга DOM с помощью lit-html. Этот метод приведён для справки. Переопределять или вызывать его не нужно. Но если вы его переопределите, обязательно вызовите super.update(changedProperties), иначе render никогда не будет вызван.
render
/**
* Implement to override default behavior.
*/
render() { ... }
| Возвращает | TemplateResult |
Должен возвращать TemplateResult lit-html. |
| Запускает обновление? | Нет | Изменения свойств внутри этого метода не запускают обновление элемента. |
Использует lit-html для рендеринга шаблона элемента. Необходимо реализовать render для любого компонента, расширяющего базовый класс LitElement.
Дополнительную информацию см. в документации по шаблонам.
firstUpdated
/**
* Implement to override default behavior.
*/
firstUpdated(changedProperties) { ... }
| Параметры | changedProperties |
Map. Ключи — имена изменившихся свойств; значения — соответствующие предыдущие значения. |
| Запускает обновление? | Да | Изменения свойств внутри этого метода запускают обновление элемента. |
Вызывается после первого обновления DOM элемента, непосредственно перед вызовом updated.
Реализуйте firstUpdated, чтобы выполнить одноразовые действия после создания шаблона элемента.
Пример: установить фокус на элементе ввода при первом обновлении
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
textAreaId: { type: String },
startingText: { type: String }
};
}
constructor() {
super();
this.textAreaId = 'myText';
this.startingText = 'Focus me on first update';
}
render() {
return html`
<textarea id="${this.textAreaId}">${this.startingText}</textarea>
`;
}
firstUpdated(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
console.log(`${propName} changed. oldValue: ${oldValue}`);
});
const textArea = this.shadowRoot.getElementById(this.textAreaId);
textArea.focus();
}
}
customElements.define('my-element', MyElement);
updated
/**
* Implement to override default behavior.
*/
updated(changedProperties) { ... }
| Параметры | changedProperties |
Map. Ключи — имена изменившихся свойств; значения — соответствующие предыдущие значения. |
| Запускает обновление? | Да | Изменения свойств внутри этого метода запускают обновление элемента. |
Вызывается после обновления и рендеринга DOM элемента. Реализуйте этот метод, чтобы выполнять задачи после обновления.
Пример: установить фокус на элементе после обновления
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
prop1: { type: Number },
prop2: { type: Number }
};
}
constructor() {
super();
this.prop1 = 0;
this.prop2 = 0;
}
render() {
return html`
<style>button:focus { background-color: aliceblue; }</style>
<p>prop1: ${this.prop1}</p>
<p>prop2: ${this.prop2}</p>
<button id="a" @click="${() => this.prop1=Math.random()}">prop1</button>
<button id="b" @click="${() => this.prop2=Math.random()}">prop2</button>
`;
}
updated(changedProperties) {
changedProperties.forEach((oldValue, propName) => {
console.log(`${propName} changed. oldValue: ${oldValue}`);
});
let b = this.shadowRoot.getElementById('b');
b.focus();
}
}
customElements.define('my-element', MyElement);
updateComplete
// Await Promise property. await this.updateComplete;
| Тип | Promise |
Разрешается со значением Boolean, когда обновление элемента завершено. |
|
Разрешается |
true, если ожидающих обновлений больше нет.false, если этот цикл обновления запустил ещё одно обновление. |
Promise updateComplete разрешается, когда обновление элемента завершено. Используйте updateComplete, чтобы дождаться обновления:
await this.updateComplete; // do stuff
this.updateComplete.then(() => { /* do stuff */ });
Пример
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
prop1: { type: Number }
};
}
constructor() {
super();
this.prop1 = 0;
}
render() {
return html`
<p>prop1: ${this.prop1}</p>
<button @click="${this.changeProp}">prop1</button>
`;
}
async getMoreState() {
return;
}
async changeProp() {
this.prop1 = Math.random();
await Promise.all([this.updateComplete, this.getMoreState()]);
console.log('Update complete. Other state completed.');
}
}
customElements.define('my-element', MyElement);
Переопределение updateComplete
Чтобы дождаться дополнительного состояния перед разрешением Promise updateComplete, переопределите метод _getUpdateComplete. Например, здесь может быть полезно дождаться обновления дочернего элемента. Сначала дождитесь super._getUpdateComplete(), а затем — любого последующего состояния.
Рекомендуется переопределять метод _getUpdateComplete, а не геттер updateComplete, чтобы обеспечить совместимость с пользователями, использующими вывод TypeScript ES5 (см. TypeScript#338).
class MyElement extends LitElement {
async _getUpdateComplete() {
await super._getUpdateComplete();
await this._myChild.updateComplete;
}
}
Примеры
Управление временем выполнения обновлений
async performUpdate() {
await new Promise((resolve) => requestAnimationFrame(() => resolve());
super.performUpdate();
}
Настройка изменений свойств, вызывающих обновление
shouldUpdate(changedProps) {
return changedProps.has('prop1');
}
Настройка определения изменения свойства
Укажите hasChanged для свойства. См. документацию по свойствам.
Управление изменениями свойств и обновлениями для вложенных свойств объектов
Мутации (изменения вложенных свойств объектов и элементов массивов) не отслеживаются. Вместо этого полностью замените объект или вызовите requestUpdate после мутации.
// Option 1: Rewrite whole object, triggering an update
this.prop1 = Object.assign({}, this.prop1, { subProp: 'data' });
// Option 2: Mutate a subproperty, then call requestUpdate
this.prop1.subProp = 'data';
this.requestUpdate();
Обновление в ответ на событие, не связанное с изменением свойства
Вызовите requestUpdate:
// Request an update in response to an event
this.addEventListener('load-complete', async (e) => {
console.log(e.detail.message);
console.log(await this.requestUpdate());
});
Запрос обновления независимо от изменений свойств
Вызовите requestUpdate():
this.requestUpdate();
Запрос обновления для определённого свойства
Вызовите requestUpdate(propName, oldValue):
let oldValue = this.prop1;
this.prop1 = 'new value';
this.requestUpdate('prop1', oldValue);
Выполнение действий после первого обновления
Реализуйте firstUpdated:
firstUpdated(changedProps) {
console.log(changedProps.get('prop1'));
}
Выполнение действий после каждого обновления
Реализуйте updated:
updated(changedProps) {
console.log(changedProps.get('prop1'));
}
Выполнение действий при следующем обновлении элемента
Дождитесь Promise updateComplete:
await this.updateComplete; // do stuff
this.updateComplete.then(() => {
// do stuff
});
Ожидание завершения обновления элемента
Дождитесь Promise updateComplete:
let done = await updateComplete;
updateComplete.then(() => {
// finished updating
});
© Google LLC
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://lit.dev/docs/v1/components/lifecycle/