Spec-Zone.ru › Lit 3

Жизненный цикл

Компоненты Lit используют стандартные методы жизненного цикла пользовательских элементов. Кроме того, Lit вводит реактивный цикл обновления, который отображает изменения в DOM при изменении реактивных свойств.

Стандартный жизненный цикл пользовательского элемента

Компоненты Lit — это стандартные пользовательские элементы, которые наследуют методы жизненного цикла пользовательских элементов. Подробнее о жизненном цикле пользовательского элемента см. в статье MDN Использование обратных вызовов жизненного цикла.

Если вам нужно настроить какой-либо из стандартных методов жизненного цикла пользовательского элемента, обязательно вызывайте реализацию super (например, super.connectedCallback()), чтобы сохранить стандартные возможности Lit.

constructor()

Вызывается при создании элемента. Также вызывается при обновлении уже существующего элемента, которое происходит, когда определение пользовательского элемента загружается после того, как элемент уже добавлен в DOM.

Поведение Lit

Запрашивает асинхронное обновление с помощью метода requestUpdate(), поэтому после обновления компонента Lit выполняется немедленное обновление.

Сохраняет любые свойства, уже заданные для элемента. Это гарантирует, что значения, заданные до обновления, сохранятся и корректно переопределят значения по умолчанию, заданные компонентом.

Варианты использования

Выполняйте здесь задачи инициализации, которые нужно выполнить один раз до первого обновления. Например, если вы не используете декораторы, значения свойств по умолчанию можно задать в конструкторе, как показано в разделе Объявление свойств в статическом поле properties.

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

connectedCallback()

Вызывается, когда компонент добавляется в DOM документа.

Поведение Lit

Lit запускает первый цикл обновления элемента после его подключения. При подготовке к отрисовке Lit также гарантирует создание renderRoot (обычно его shadowRoot).

После того как элемент хотя бы один раз подключился к документу, обновления компонента будут выполняться независимо от состояния подключения элемента.

Варианты использования

В connectedCallback() следует настраивать задачи, которые нужно выполнять только тогда, когда элемент подключен к документу. Чаще всего это добавление обработчиков событий к узлам вне элемента, например обработчика события keydown, добавленного к window. Как правило, всё, что выполняется в connectedCallback(), следует отменить при отключении элемента — например, удалить обработчики событий на window, чтобы предотвратить утечки памяти.

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

disconnectedCallback()

Вызывается, когда компонент удаляется из DOM документа.

Поведение Lit

Приостанавливает реактивный цикл обновления. Он возобновляется при подключении элемента.

Варианты использования

Этот обратный вызов служит основным сигналом для элемента о том, что он может больше не использоваться; поэтому disconnectedCallback() должен гарантировать, что ничто не удерживает ссылку на элемент (например, обработчики событий, добавленные к узлам вне элемента), чтобы сборщик мусора мог освободить его. Поскольку элементы могут повторно подключаться после отключения, например при перемещении элемента в DOM или его кэшировании, такие ссылки или обработчики может потребоваться восстановить с помощью connectedCallback(), чтобы элемент продолжал работать в этих сценариях. Например, удалите обработчики событий с узлов вне элемента, такие как обработчик события keydown, добавленный к window.

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

Внутренние обработчики событий удалять не нужно. Вам не нужно удалять обработчики событий, добавленные в собственный DOM компонента, в том числе декларативно заданные в шаблоне. В отличие от внешних обработчиков событий, они не мешают сборщику мусора освободить компонент.

attributeChangedCallback()

Вызывается при изменении одного из observedAttributes элемента.

Поведение Lit

Lit использует этот обратный вызов для синхронизации изменений атрибутов с реактивными свойствами. В частности, при установке атрибута устанавливается соответствующее свойство. Lit также автоматически настраивает массив observedAttributes элемента в соответствии со списком реактивных свойств компонента.

Варианты использования

Обычно реализовывать этот обратный вызов не требуется.

adoptedCallback()

Вызывается, когда компонент перемещается в новый документ.

Обратите внимание: adoptedCallback не полифилится.

Поведение Lit

По умолчанию Lit не выполняет никаких действий в этом обратном вызове.

Варианты использования

Этот обратный вызов следует использовать только в сложных сценариях, когда поведение элемента должно меняться при его перемещении в другой документ.

Реактивный цикл обновления

Помимо стандартного жизненного цикла пользовательского элемента, компоненты Lit реализуют реактивный цикл обновления.

Реактивный цикл обновления запускается при изменении реактивного свойства или явном вызове метода requestUpdate(). Lit выполняет обновления асинхронно, поэтому изменения свойств объединяются в группы: если после запроса обновления, но до его начала изменятся дополнительные свойства, все изменения будут учтены в одном обновлении.

Обновления выполняются во время микрозадач, то есть до того, как браузер отрисует следующий кадр на экране. Подробнее о синхронизации браузера см. в статье Jake Archibald о микрозадачах.

В целом реактивный цикл обновления выглядит так:

  1. Обновление планируется при изменении одного или нескольких свойств или при вызове requestUpdate().
  2. Обновление выполняется до отрисовки следующего кадра.
    1. Устанавливаются отражаемые атрибуты.
    2. Вызывается метод render компонента для обновления его внутреннего DOM.
  3. Обновление завершается, и выполняется разрешение промиса updateComplete.

Более подробно это выглядит так:

До обновления

Обновление

После обновления

Карта changedProperties

Многие методы реактивного обновления получают Map изменённых свойств. Ключи Map — это имена свойств, а их значения — предыдущие значения свойств. Текущие значения свойств всегда можно получить с помощью this.property или this[property].

Типы TypeScript для changedProperties

Если вы используете 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() выполняет проверку на строгое равенство; если результат — true, планируется обновление. Подробнее см. в разделе Настройка hasChanged().

requestUpdate()

Вызовите requestUpdate(), чтобы явно запланировать обновление. Это может пригодиться, если нужно, чтобы элемент обновился и выполнил отрисовку при изменении чего-либо, не связанного со свойством. Например, компонент таймера может вызывать requestUpdate() каждую секунду.

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

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

Список изменившихся свойств хранится в карте changedProperties, которая передаётся последующим методам жизненного цикла. Ключи карты — это имена свойств, а их значения — предыдущие значения свойств.

При вызове requestUpdate() можно также передать имя свойства и предыдущее значение; они будут сохранены в карте changedProperties. Это может пригодиться, если вы реализуете собственные методы получения и установки значения свойства. Подробнее о реализации собственных методов получения и установки значений см. в разделе Реактивные свойства.

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

Выполнение обновления

При выполнении обновления вызывается метод performUpdate(). Этот метод вызывает несколько других методов жизненного цикла.

Изменения, которые обычно запускают обновление, но происходят во время обновления компонента, не планируют новое обновление. Это необходимо, чтобы значения свойств можно было вычислять в процессе обновления. Свойства, изменённые во время обновления, отражаются в карте changedProperties, поэтому последующие методы жизненного цикла могут учитывать эти изменения.

shouldUpdate()

Вызывается, чтобы определить, требуется ли цикл обновления.

Аргументы changedProperties: Map с именами изменённых свойств в качестве ключей и соответствующими предыдущими значениями в качестве значений.
Запускает обновление Нет. Изменения свойств внутри этого метода не вызывают обновление элемента.
Вызывать super? Не требуется.
Вызывается на сервере? Нет.

Если shouldUpdate() возвращает true, как и предусмотрено по умолчанию, обновление продолжается обычным образом. Если метод возвращает false, остальные этапы цикла обновления не выполняются, но промис updateComplete всё равно разрешается.

Реализуйте 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()

Вызывается перед update() для вычисления значений, необходимых во время обновления.

Аргументы changedProperties: Map с именами изменённых свойств в качестве ключей и соответствующими предыдущими значениями в качестве значений.
Запускает обновление? Нет. Изменения свойств внутри этого метода не вызывают обновление элемента.
Вызывать super? Не требуется.
Вызывается на сервере? Да.

Реализуйте 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()

Вызывается для обновления DOM компонента.

Аргументы changedProperties: Map с именами изменённых свойств в качестве ключей и соответствующими предыдущими значениями в качестве значений.
Запускает обновление? Нет. Изменения свойств внутри этого метода не вызывают обновление элемента.
Вызывать super? Да. Без вызова super атрибуты элемента и шаблон не обновятся.
Вызывается на сервере? Нет.

Отражает значения свойств в атрибутах и вызывает render() для обновления внутреннего DOM компонента.

Как правило, реализовывать этот метод не требуется.

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()

Вызывается после первого обновления DOM компонента, непосредственно перед вызовом updated().

Аргументы changedProperties: Map с именами изменённых свойств в качестве ключей и соответствующими предыдущими значениями в качестве значений.
Запускает обновление? Да. Изменения свойств внутри этого метода планируют новый цикл обновления.
Вызывать super? Не требуется.
Вызывается на сервере? Нет.

Реализуйте firstUpdated(), чтобы выполнить одноразовые действия после создания DOM компонента. Например, можно установить фокус на определённый отрисованный элемент или добавить к элементу ResizeObserver либо IntersectionObserver.

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

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

Промис updateComplete разрешается, когда обновление элемента завершается. Используйте updateComplete, чтобы дождаться обновления. Разрешённое значение — логическое значение, указывающее, завершил ли элемент обновление. Оно будет равно true, если после завершения цикла обновления нет ожидающих обновлений.

Обновление элемента может вызвать обновление его дочерних элементов. По умолчанию промис updateComplete разрешается после завершения обновления самого элемента, не дожидаясь завершения обновлений его дочерних элементов. Это поведение можно настроить, переопределив getUpdateComplete.

Знать о завершении обновления элемента может потребоваться в нескольких случаях:

  1. Тестирование При написании тестов можно дождаться разрешения промиса updateComplete, прежде чем проверять DOM компонента. Если проверки зависят от завершения обновлений всего дерева потомков компонента, часто лучше дождаться requestAnimationFrame, поскольку планирование по умолчанию в Lit использует очередь микрозадач браузера, которая очищается до отрисовки кадров анимации. Это гарантирует, что все ожидающие обновления Lit на странице завершатся до вызова обратного вызова requestAnimationFrame.

  2. Измерение Некоторым компонентам требуется измерять DOM для реализации определённых макетов. Макеты всегда лучше реализовывать с помощью чистого CSS, а не измерений на JavaScript, но иногда ограничения CSS не позволяют этого избежать. В очень простых случаях, если вы измеряете компоненты Lit или ReactiveElement, может быть достаточно дождаться updateComplete после изменения состояния и перед измерением. Однако, поскольку updateComplete не дожидается обновления всех потомков, мы рекомендуем использовать ResizeObserver как более надёжный способ запускать код измерения при изменении макетов.

  3. События Хорошей практикой считается отправлять события из компонентов после завершения отрисовки, чтобы обработчики события получали полностью отрисованное состояние компонента. Для этого перед отправкой события можно дождаться промиса updateComplete.

    async _loginClickHandler() {
      this.loggedIn = true;
      // Wait for `loggedIn` state to be rendered to the DOM
      await this.updateComplete;
      this.dispatchEvent(new Event('login'));
    }

Промис updateComplete отклоняется, если во время цикла обновления возникает необработанная ошибка. Подробнее см. в разделе Обработка ошибок в цикле обновления.

Обработка ошибок в цикле обновления

Если в методе жизненного цикла, например render() или update(), возникает необработанное исключение, промис updateComplete отклоняется. Если код в методе жизненного цикла может вызвать исключение, рекомендуется поместить его в блок try/catch.

Если вы ожидаете разрешения промиса updateComplete, можно также использовать try/catch:

try {
  await this.updateComplete;
} catch (e) {
  /* handle error */
}

В некоторых случаях ошибка может возникнуть в неожиданном месте. В качестве запасного варианта можно добавить обработчик для window.onunhandledrejection, чтобы перехватывать такие ошибки. Например, можно использовать его для отправки отчётов об ошибках в серверную службу, чтобы помочь диагностировать проблемы, которые трудно воспроизвести.

window.onunhandledrejection = function(e) {
  /* handle error */
}

Дополнительная настройка

В этом разделе рассматриваются некоторые менее распространённые методы настройки цикла обновления.

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() для выполнения ожидающего обновления.

Белая буква i в синем круге

Асинхронная функция необязательна.

В этом примере показана асинхронная функция, которая неявно возвращает промис. Также можно записать scheduleUpdate() как функцию, которая явно возвращает Promise. В обоих случаях следующее обновление не начнётся, пока не разрешится промис, возвращённый методом scheduleUpdate().

performUpdate()

Реализует реактивный цикл обновления, вызывая другие методы, например shouldUpdate(), update() и updated().

Вызовите performUpdate(), чтобы немедленно обработать ожидающее обновление. Обычно это не требуется, но в редких случаях может понадобиться синхронное обновление. (Если обновление не ожидается, можно вызвать requestUpdate(), а затем performUpdate(), чтобы принудительно выполнить синхронное обновление.)

Белая буква i в синем круге

Для настройки планирования используйте scheduleUpdate().

Чтобы настроить планирование обновлений, переопределите scheduleUpdate(). Ранее для этой цели мы рекомендовали переопределять performUpdate(). Это по-прежнему работает, но усложняет вызов performUpdate() для синхронной обработки ожидающего обновления.

hasUpdated

Свойство hasUpdated возвращает true, если компонент обновлялся хотя бы один раз. Используйте hasUpdated в любом из методов жизненного цикла, чтобы выполнять действия, только если компонент ещё не обновлялся.

getUpdateComplete()

Чтобы дождаться выполнения дополнительных условий перед разрешением промиса updateComplete, переопределите метод getUpdateComplete(). Например, может быть полезно дождаться обновления дочернего элемента. Сначала дождитесь super.getUpdateComplete(), а затем — любого последующего состояния.

Рекомендуется переопределять метод getUpdateComplete(), а не геттер updateComplete, чтобы обеспечить совместимость с пользователями, использующими вывод 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()

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() добавляет реактивный контроллер в компонент Lit, чтобы компонент вызывал обратные вызовы жизненного цикла контроллера. Дополнительные сведения см. в документации по реактивным контроллерам.

removeController()

removeController() удаляет реактивный контроллер, и он перестаёт получать обратные вызовы жизненного цикла от этого компонента.

Реактивный цикл обновления на сервере

Пакет Lit для серверной отрисовки сейчас активно разрабатывается, поэтому приведённая ниже информация может измениться.

При отрисовке Lit на сервере выполняются не все этапы цикла обновления. На сервере вызываются следующие методы.

Изменить эту страницу

© Google LLC
Licensed under the Creative Commons Attribution 3.0 Unported License.
https://lit.dev/docs/components/lifecycle/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API