Spec-Zone.ru › Lit 2

Реактивные свойства

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

class MyElement extends LitElement {
  @property()
  name?: string;
}
class MyElement extends LitElement {
  static properties = {
    name: {},
  };
}

Lit управляет реактивными свойствами и соответствующими им атрибутами. В частности:

  • Реактивные обновления. Lit создает пару геттер/сеттер для каждого реактивного свойства. При изменении реактивного свойства компонент планирует обновление.
  • Обработка атрибутов. По умолчанию Lit настраивает отслеживаемый атрибут, соответствующий свойству, и обновляет свойство при изменении атрибута. При необходимости значения свойств также могут отражаться обратно в атрибут.
  • Свойства суперкласса. Lit автоматически применяет параметры свойств, объявленные суперклассом. Повторно объявлять свойства не нужно, если только вы не хотите изменить параметры.
  • Апгрейд элемента. Если компонент Lit определяется после того, как элемент уже находится в DOM, Lit обрабатывает логику апгрейда и гарантирует, что свойства, заданные элементу до апгрейда, вызовут нужные побочные эффекты реактивности после его апгрейда.

Публичные свойства и внутреннее состояние

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

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

Lit также поддерживает внутреннее реактивное состояние. Под внутренним реактивным состоянием понимаются реактивные свойства, которые не являются частью API компонента. Эти свойства не имеют соответствующего атрибута и обычно помечаются в TypeScript как protected или private.

@state()
private _counter = 0;
static properties = {
  _counter: {state: true};
};

constructor() {
  super();
  this._counter = 0;
}

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

Как и изменение публичных реактивных свойств, обновление внутреннего реактивного состояния запускает цикл обновления. Подробнее см. в разделе Внутреннее реактивное состояние.

Публичные реактивные свойства

Объявляйте публичные реактивные свойства элемента с помощью декораторов или статического поля properties.

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

Объявление свойств с помощью декораторов

Используйте декоратор @property вместе с объявлением поля класса, чтобы объявить реактивное свойство.

class MyElement extends LitElement {
  @property({type: String})
  mode?: string;

  @property({attribute: false})
  data = {};
}

Аргумент декораторов @property — это объект параметров. Если аргумент не указан, это равносильно указанию значений по умолчанию для всех параметров.

Использование декораторов. Декораторы — это предлагаемая возможность JavaScript, поэтому для их использования потребуется компилятор, например Babel или компилятор TypeScript. Подробнее см. в разделе Включение декораторов.

Объявление свойств в статическом поле класса properties

Чтобы объявить свойства в статическом поле класса properties:

class MyElement extends LitElement {
  static properties = {
    mode: {type: String},
    data: {attribute: false},
  };

  constructor() {
    super();
    this.data = {};
  }
}

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

Как избежать проблем с полями класса при объявлении свойств

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

В JavaScript нельзя использовать поля класса при объявлении реактивных свойств. Вместо этого свойства нужно инициализировать в конструкторе элемента:

constructor() {
  super();
  this.data = {};
}

В TypeScript для объявления реактивных свойств можно использовать поля класса, если вы применяете один из следующих подходов:

  • Установите параметр useDefineForClassFields в файле tsconfig в значение false. Обратите внимание: для некоторых конфигураций TypeScript это не обязательно, но рекомендуется явно задать значение false.
  • Добавьте к полю ключевое слово declare, а инициализатор поля поместите в конструктор.

При компиляции JavaScript с помощью Babel для объявления реактивных свойств можно использовать поля класса, если в конфигурации assumptions файла babelrc установить setPublicClassFields в значение true. Обратите внимание: для старых версий Babel также необходимо подключить плагин @babel/plugin-proposal-class-properties:

assumptions = {
  "setPublicClassFields": true
};

plugins = [
  ["@babel/plugin-proposal-class-properties"],
];

Подробнее об использовании полей класса с декораторами см. в разделе Как избежать проблем с полями класса и декораторами.

Параметры свойств

Объект параметров может содержать следующие свойства:

attribute

Определяет, связан ли атрибут со свойством, или задает пользовательское имя связанного атрибута. По умолчанию: true. Если attribute имеет значение false, параметры converter, reflect и type игнорируются. Подробнее см. в разделе Настройка имени атрибута.

converter

Пользовательский преобразователь для преобразования между свойствами и атрибутами. Если он не указан, используется преобразователь атрибутов по умолчанию.

hasChanged

Функция, вызываемая при каждом присваивании свойству, чтобы определить, изменилось ли оно и нужно ли запускать обновление. Если функция не указана, LitElement использует проверку строгого неравенства (newValue !== oldValue), чтобы определить, изменилось ли значение свойства. Подробнее см. в разделе Настройка обнаружения изменений.

noAccessor

Задайте значение true, чтобы запретить создание аксессоров свойств по умолчанию. Этот параметр требуется редко. По умолчанию: false. Подробнее см. в разделе Запрет Lit на создание аксессора свойства.

reflect

Определяет, отражается ли значение свойства обратно в связанный атрибут. По умолчанию: false. Подробнее см. в разделе Включение отражения в атрибут.

state

Задайте значение true, чтобы объявить свойство как внутреннее реактивное состояние. Внутреннее реактивное состояние запускает обновления так же, как публичные реактивные свойства, но Lit не создает для него атрибут, и пользователям не следует обращаться к нему извне компонента. Эквивалент использования декоратора @state. По умолчанию: false. Подробнее см. в разделе Внутреннее реактивное состояние.

type

При преобразовании атрибута со строковым значением в свойство преобразователь атрибутов Lit по умолчанию преобразует строку в указанный тип; обратное преобразование выполняется при отражении свойства в атрибут. Если задан параметр converter, это поле передается преобразователю. Если type не указан, преобразователь по умолчанию рассматривает значение как type: String. См. раздел Использование преобразователя по умолчанию.

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

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

Внутреннее реактивное состояние

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

Используйте декоратор @state, чтобы объявить внутреннее реактивное состояние:

@state()
protected _active = false;

С помощью статического поля класса properties можно объявить внутреннее реактивное состояние, используя параметр state: true.

static properties = {
  _active: {state: true}
};

constructor() {
  this._active = false;
}

Не следует обращаться к внутреннему реактивному состоянию извне компонента. В TypeScript эти свойства должны быть помечены как private или protected. Для пользователей JavaScript мы также рекомендуем применять соглашение, например начинать имя со знака подчеркивания (_), чтобы обозначать private- или protected-свойства.

Внутреннее реактивное состояние работает так же, как публичные реактивные свойства, за исключением того, что с ним не связан атрибут. Для внутреннего реактивного состояния можно указать только функцию hasChanged.

Декоратор @state также может служить подсказкой для минификатора кода о том, что имя свойства можно изменить при минификации.

Что происходит при изменении свойств

Изменение свойства может запустить цикл реактивного обновления, в результате которого шаблон компонента будет отрендерен повторно.

При изменении свойства происходит следующее:

  1. Вызывается сеттер свойства.
  2. Сеттер вызывает метод requestUpdate компонента.
  3. Сравниваются старое и новое значения свойства.
    • По умолчанию Lit использует проверку строгого неравенства, чтобы определить, изменилось ли значение (то есть newValue !== oldValue).
    • Если у свойства есть функция hasChanged, она вызывается со старым и новым значениями свойства.
  4. Если изменение свойства обнаружено, асинхронно планируется обновление. Если обновление уже запланировано, выполняется только одно обновление.
  5. Вызывается метод update компонента, который отражает измененные свойства в атрибутах и повторно рендерит шаблоны компонента.

Обратите внимание: изменение объекта или массива в свойстве не запускает обновление, поскольку сам объект не изменился. Подробнее см. в разделе Изменение объектов и массивов в свойствах.

Существует множество способов подключиться к циклу реактивного обновления и изменить его. Подробнее см. в разделе Цикл реактивного обновления.

Подробнее об обнаружении изменений свойств см. в разделе Настройка обнаружения изменений.

Изменение объектов и массивов в свойствах

Изменение объекта или массива не меняет ссылку на него, поэтому обновление не запускается. Работа со свойствами-объектами и свойствами-массивами возможна двумя способами:

  • Подход с неизменяемыми данными. Считайте объекты и массивы неизменяемыми. Например, чтобы удалить элемент из myArray, создайте новый массив:

    this.myArray = this.myArray.filter((_, i) => i !== indexToRemove);

    Этот пример прост, но для управления неизменяемыми данными часто удобно использовать библиотеку, например Immer. Она поможет избежать сложного шаблонного кода при изменении глубоко вложенных объектов.

  • Запуск обновления вручную. Измените данные и вызовите requestUpdate(), чтобы непосредственно запустить обновление. Например:

    this.myArray.splice(indexToRemove, 1);
    this.requestUpdate();

    При вызове без аргументов requestUpdate() планирует обновление, не вызывая функцию hasChanged(). Но имейте в виду: requestUpdate() обновляет только текущий компонент. То есть, если компонент использует код из примера выше и передает this.myArray вложенному компоненту, вложенный компонент обнаружит, что ссылка на массив не изменилась, и не обновится.

Как правило, для большинства приложений лучше всего подходит нисходящий поток данных с неизменяемыми объектами. Это гарантирует, что каждый компонент, которому нужно отобразить новое значение, сделает это (и сделает это максимально эффективно, поскольку неизменившиеся части дерева данных не вызовут обновления зависящих от них компонентов).

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

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

Атрибуты

Свойства отлично подходят для получения данных JavaScript в качестве входных данных, а атрибуты — стандартный способ настройки элементов в HTML-разметке, без необходимости использовать JavaScript для задания свойств. Возможность задавать реактивные свойства как через свойства, так и через атрибуты позволяет использовать компоненты Lit в самых разных средах, в том числе там, где разметка отображается без клиентского шаблонизатора, например на статических HTML-страницах, предоставляемых CMS.

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

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

  • Чтобы отслеживать атрибут (задавать свойство из атрибута), значение атрибута необходимо преобразовать из строки в соответствии с типом свойства.

  • Чтобы отражать атрибут (задавать атрибут из свойства), значение свойства необходимо преобразовать в строку.

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

Настройка имени атрибута

По умолчанию Lit создает соответствующий отслеживаемый атрибут для всех публичных реактивных свойств. Имя отслеживаемого атрибута совпадает с именем свойства, записанным строчными буквами:

// observed attribute name is "myvalue"
@property({ type: Number })
myValue = 0;
// observed attribute name is "myvalue"
static properties = {
  myValue: { type: Number },
};

constructor() {
  super();
  this.myValue = 0;
}

Чтобы создать отслеживаемый атрибут с другим именем, задайте attribute строковое значение:

// Observed attribute will be called my-name
@property({ attribute: 'my-name' })
myName = 'Ogden';
// Observed attribute will be called my-name
static properties = {
  myName: { attribute: 'my-name' },
};

constructor() {
  super();
  this.myName = 'Ogden'
}

Чтобы не создавать отслеживаемый атрибут для свойства, задайте attribute значение false. Свойство не будет инициализироваться из атрибутов в разметке, а изменения атрибута не будут на него влиять.

// No observed attribute for this property
@property({ attribute: false })
myData = {};
// No observed attribute for this property
static properties = {
  myData: { attribute: false },
};

constructor() {
  super();
  this.myData = {};
}

С внутренним реактивным состоянием атрибут никогда не связан.

Отслеживаемый атрибут можно использовать, чтобы задать начальное значение свойства в разметке. Например:

<my-element myvalue="99"></my-element>

Использование преобразователя по умолчанию

В Lit предусмотрен преобразователь по умолчанию для свойств типов String, Number, Boolean, Array и Object.

Чтобы использовать преобразователь по умолчанию, укажите параметр type в объявлении свойства:

// Use the default converter
@property({ type: Number })
count = 0;
// Use the default converter
static properties = {
  count: { type: Number },
};

constructor() {
  super();
  this.count = 0;
}

Если для свойства не указан ни тип, ни пользовательский преобразователь, поведение будет таким, как если бы было задано type: String.

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

Из атрибута в свойство

Тип Преобразование
String Если у элемента есть соответствующий атрибут, задайте свойству значение атрибута.
Number Если у элемента есть соответствующий атрибут, задайте свойству значение Number(attributeValue).
Boolean Если у элемента есть соответствующий атрибут, задайте свойству значение true.
Если атрибута нет, задайте свойству значение false.
Object, Array Если у элемента есть соответствующий атрибут, задайте свойству значение JSON.parse(attributeValue).

Во всех случаях, кроме Boolean, если у элемента нет соответствующего атрибута, свойство сохраняет значение по умолчанию или получает значение undefined, если значение по умолчанию не задано.

Из свойства в атрибут

Тип Преобразование
String, Number Если свойство определено и не равно null, задайте атрибуту значение свойства.
Если свойство равно null или undefined, удалите атрибут.
Boolean Если значение свойства истинное, создайте атрибут и задайте ему пустую строку.
Если значение свойства ложное, удалите атрибут.
Object, Array Если свойство определено и не равно null, задайте атрибуту значение JSON.stringify(propertyValue).
Если свойство равно null или undefined, удалите атрибут.

Предоставление пользовательского преобразователя

В объявлении свойства можно указать пользовательский преобразователь свойств с помощью параметра converter:

myProp: {
  converter: // Custom property converter
}

converter может быть объектом или функцией. Если это объект, он может содержать ключи fromAttribute и toAttribute:

prop1: {
  converter: {
    fromAttribute: (value, type) => {
      // `value` is a string
      // Convert it to a value of type `type` and return it
    },
    toAttribute: (value, type) => {
      // `value` is of type `type`
      // Convert it to a string and return it
    }
  }
}

Если converter — это функция, она используется вместо fromAttribute:

myProp: {
  converter: (value, type) => {
    // `value` is a string
    // Convert it to a value of type `type` and return it
  }
}

Если для отражаемого атрибута функция toAttribute не задана, атрибуту присваивается значение свойства с помощью преобразователя по умолчанию.

Если toAttribute возвращает null или undefined, атрибут удаляется.

Логические атрибуты

Чтобы логическое свойство можно было настраивать через атрибут, его значением по умолчанию должно быть false. Если значение по умолчанию — true, задать ему значение false в разметке нельзя, поскольку наличие атрибута — независимо от того, задано ли у него значение, — означает true. Именно так атрибуты работают в веб-платформе.

Если такое поведение не подходит для вашего случая, можно поступить одним из следующих способов:

  • Изменить имя свойства так, чтобы по умолчанию оно имело значение false. Например, веб-платформа использует атрибут disabled (по умолчанию — false), а не enabled.

  • Вместо этого использовать атрибут со строковым или числовым значением.

Включение отражения в атрибут

Можно настроить свойство так, чтобы при каждом его изменении значение отражалось в соответствующем атрибуте. Отражаемые атрибуты полезны, поскольку они доступны CSS и таким API DOM, как querySelector.

Например:

// Value of property "active" will reflect to attribute "active"
active: {reflect: true}

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

Как правило, атрибуты следует считать входными данными, которые владелец элемента передает ему, а не данными, которыми управляет сам элемент. Поэтому отражать свойства в атрибутах следует умеренно. Сейчас это необходимо, например, для стилизации и обеспечения доступности, но ситуация, вероятно, изменится по мере появления в платформе таких возможностей, как псевдоселектор :state и модель объектов доступности, которые устраняют эти пробелы.

Не рекомендуется отражать в атрибутах свойства типа object или array. Это может привести к сериализации больших объектов в DOM и ухудшить производительность.

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

Пользовательские аксессоры свойств

По умолчанию LitElement создает пару геттер/сеттер для всех реактивных свойств. Сеттер вызывается при каждом присваивании значения свойству:

// Declare a property
@property()
greeting: string = 'Hello';
...
// Later, set the property
this.greeting = 'Hola'; // invokes greeting's generated property accessor
// Declare a property
static properties = {
  greeting: {},
}
constructor() {
  this.super();
  this.greeting = 'Hello';
}
...
// Later, set the property
this.greeting = 'Hola'; // invokes greeting's generated property accessor

Созданные аксессоры автоматически вызывают requestUpdate(), запуская обновление, если оно еще не началось.

Создание пользовательских аксессоров свойств

Чтобы задать способ чтения и записи свойства, можно определить собственную пару геттер/сеттер. Например:

private _prop = 0;

set prop(val: number) {
  let oldVal = this._prop;
  this._prop = Math.floor(val);
  this.requestUpdate('prop', oldVal);
}

@property()
get prop() { return this._prop; }
static properties = {
  prop: {},
};

_prop = 0;

set prop(val) {
  let oldVal = this._prop;
  this._prop = Math.floor(val);
  this.requestUpdate('prop', oldVal);
}

get prop() { return this._prop; }

Чтобы использовать пользовательские аксессоры свойств с декораторами @property или @state, поместите декоратор над геттером, как показано выше.

Сеттеры, автоматически создаваемые Lit, вызывают requestUpdate(). Если вы пишете собственный сеттер, необходимо вручную вызывать requestUpdate(), передавая ему имя свойства и его старое значение.

В большинстве случаев создавать пользовательские аксессоры свойств не нужно. Чтобы вычислять значения на основе существующих свойств, рекомендуем использовать обратный вызов willUpdate, который позволяет задавать значения во время цикла обновления, не запуская дополнительное обновление. Чтобы выполнить пользовательское действие после обновления элемента, рекомендуем использовать обратный вызов updated. Пользовательский сеттер может пригодиться в редких случаях, когда важно синхронно проверять любое значение, задаваемое пользователем.

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

Запрет Lit на создание аксессора свойства

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

Чтобы Lit не создавал аксессор свойства, который перезаписал бы аксессор, определенный в суперклассе, задайте noAccessor значение true в объявлении свойства:

static properties = {
  myProp: { type: Number, noAccessor: true }
};

Задавать noAccessor при определении собственных аксессоров не нужно.

Настройка обнаружения изменений

У всех реактивных свойств есть функция hasChanged(), которая вызывается при присваивании значения свойству.

Функция hasChanged сравнивает старое и новое значения свойства и определяет, изменилось ли оно. Если hasChanged() возвращает true, Lit запускает обновление элемента, если оно еще не запланировано. Подробнее об обновлениях см. в разделе Цикл реактивного обновления.

Реализация hasChanged() по умолчанию использует сравнение на строгое неравенство: hasChanged() возвращает true, если newVal !== oldVal.

Чтобы настроить hasChanged() для свойства, укажите эту функцию в качестве параметра свойства:

@property({
  hasChanged(newVal: string, oldVal: string) {
    return newVal?.toLowerCase() !== oldVal?.toLowerCase();
  }
})
myProp: string | undefined;
static properties = {
  myProp: {
    hasChanged(newVal, oldVal) {
      return newVal?.toLowerCase() !== oldVal?.toLowerCase();
    }
  }
};

В следующем примере hasChanged() возвращает true только для нечетных значений.

Редактировать эту страницу

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

Spec-Zone.ru

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