Перейти к содержанию

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

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

1
2
3
4
class MyElement extends LitElement {
    @property()
    name?: string;
}
1
2
3
4
5
class MyElement extends LitElement {
    static properties = {
        name: {},
    };
}

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

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

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

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

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

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

1
2
@state()
private _counter = 0;
1
2
3
4
5
6
7
8
static properties = {
    _counter: {state: true}
};

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

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

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

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

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

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

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

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

1
2
3
4
5
6
7
class MyElement extends LitElement {
    @property({ type: String })
    mode?: string;

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

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

Использование декораторов

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

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class MyElement extends LitElement {
    static properties = {
        mode: { type: String },
        data: { attribute: false },
    };

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

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

Избегание проблем с полями классов при объявлении свойств

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

1
2
3
4
class MyElement extends LitElement {
    static properties = { foo: { type: String } };
    foo = 'Default'; // ❌ this will make `foo` not reactive
}

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

1
2
3
4
5
6
7
class MyElement extends LitElement {
    static properties = { foo: { type: String } };
    constructor() {
        super();
        this.foo = 'Default';
    }
}

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

1
2
3
4
class MyElement extends LitElement {
    @property()
    accessor foo = 'Default';
}

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

  • Установите опцию компилятора useDefineForClassFields в false. Это уже является рекомендацией при использовании декораторов с TypeScript.

    1
    2
    3
    4
    5
    6
    7
    // tsconfig.json
    {
        "compilerOptions": {
            "experimentalDecorators": true, // If using decorators
            "useDefineForClassFields": false
        }
    }
    

    1
    2
    3
    4
    5
    6
    7
    class MyElement extends LitElement {
        static properties = { foo: { type: String } };
        foo = 'Default';
    
        @property()
        bar = 'Default';
    }
    
  • Добавьте ключевое слово declare для поля и поместите инициализатор поля в конструктор.

    1
    2
    3
    4
    5
    6
    7
    8
    class MyElement extends LitElement {
        declare foo: string;
        static properties = { foo: { type: String } };
        constructor() {
            super();
            this.foo = 'Default';
        }
    }
    
  • Добавьте ключевое слово accessor в поле, чтобы использовать auto-accessors.

    1
    2
    3
    4
    5
    6
    7
    class MyElement extends LitElement {
        static properties = { foo: { type: String } };
        accessor foo = 'Default';
    
        @property()
        accessor bar = 'Default';
    }
    

Свойства опций

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

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 используется в runtime Lit'а для сериализации/десериализации строк, и ее не следует путать с механизмом проверки типов.

Отсутствие объекта options или указание пустого объекта options эквивалентно указанию значения по умолчанию для всех опций.

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

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

Для объявления внутреннего реактивного состояния используйте декоратор @state:

1
2
@state()
protected _active = false;

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

1
2
3
4
5
6
7
static properties = {
  _active: {state: true}
};

constructor() {
  this._active = false;
}

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

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

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

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

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

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

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

Обратите внимание, что если вы измените свойство объекта или массива, это не вызовет обновления, поскольку сам объект не изменился. Для получения дополнительной информации смотрите Мутирование свойств объектов и массивов.

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

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

Мутирование свойств объектов и массивов

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

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

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

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

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

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

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

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

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

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

Атрибуты

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

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

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

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

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

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

Установка имени атрибута

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

1
2
3
// observed attribute name is "myvalue"
@property({ type: Number })
myValue = 0;
1
2
3
4
5
6
7
8
9
// observed attribute name is "myvalue"
static properties = {
    myValue: { type: Number },
};

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

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

1
2
3
// Observed attribute will be called my-name
@property({ attribute: 'my-name' })
myName = 'Ogden';
1
2
3
4
5
6
7
8
9
// Observed attribute will be called my-name
static properties = {
    myName: { attribute: 'my-name' },
};

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

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

1
2
3
// No observed attribute for this property
@property({ attribute: false })
myData = {};
1
2
3
4
5
6
7
8
9
// No observed attribute for this property
static properties = {
    myData: { attribute: false },
};

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

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

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

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

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

Lit имеет конвертер по умолчанию, который обрабатывает типы свойств String, Number, Boolean, Array и Object.

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

1
2
3
// Use the default converter
@property({ type: Number })
count = 0;
1
2
3
4
5
6
7
8
9
// 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 или не определено, удалите атрибут.
Boolean Если свойство истинно, создайте атрибут и установите его значение в пустую строку. Если свойство ложное, удалите атрибут
Object, Array Если свойство определено и не является нулевым, установите атрибут в JSON.stringify(propertyValue). Если свойство равно null или не определено, удалите атрибут.

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

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

1
2
3
myProp: {
  converter: // Custom property converter
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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:

1
2
3
4
5
6
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 и в DOM API, таких как querySelector.

Например:

1
2
3
4
// Value of property "active" will reflect to attribute "active"
active: {
    reflect: true;
}

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

Атрибуты, как правило, считаются вводимыми в элемент от его владельца, а не контролируемыми самим элементом, поэтому отражение свойств в атрибуты следует делать крайне редко. Сегодня это необходимо для таких случаев, как стилизация и доступность, но ситуация может измениться, поскольку платформа добавляет такие функции, как псевдо-селектор :state и Accessibility Object Model, которые заполняют эти пробелы.

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

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

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

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

1
2
3
4
5
6
// Declare a property
@property()
greeting: string = 'Hello';
// ...
// Later, set the property
this.greeting = 'Hola'; // invokes greeting's generated property accessor
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 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(), инициируя обновление, если оно еще не началось.

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

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

1
2
3
4
5
6
7
8
private _prop = 0;

@property()
set prop(val: number) {
    this._prop = Math.floor(val);
}

get prop() { return this._prop; }
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
static properties = {
    prop: {},
};

_prop = 0;

set prop(val) {
    this._prop = Math.floor(val);
}

get prop() { return this._prop; }

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

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

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

Запретить Lit генерировать аксессоры для свойств

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

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

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

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

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

Все реактивные свойства имеют функцию hasChanged(), которая вызывается, когда свойство установлено.

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

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

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

1
2
3
4
5
6
@property({
    hasChanged(newVal: string, oldVal: string) {
        return newVal?.toLowerCase() !== oldVal?.toLowerCase();
    }
})
myProp: string | undefined;
1
2
3
4
5
6
7
static properties = {
    myProp: {
        hasChanged(newVal, oldVal) {
        return newVal?.toLowerCase() !== oldVal?.toLowerCase();
        }
    }
};

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

Комментарии