Статьи

Угловая, неизменность и инкапсуляция

Использование изменяемых объектов для моделирования состояния приложения затрудняет отслеживание изменений и приводит к значительным потерям производительности. Переход на неизменяемые объекты решает эти проблемы, но приносит новые. Это потому, что неизменяемость и инкапсуляция часто расходятся. Можем ли мы объединить преимущества неизменности и местного государства? В этой статье я расскажу, как это можно сделать в Angular 2.

Проблемы с изменяемыми объектами

Отслеживание изменений

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

@Component({
  selector: 'person',
  template: `{{person.name}} lives at {{address.street}}, {{address.city}}`
})
class DisplayPerson {
  @Input() person: {name:string};
  @Input() address: {city:string, street:string};
}

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

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

@Component({
  selector: 'person',
  template: `{{person.name}} lives at {{address.street}}, {{address.city}} (zipCode)`
})
class DisplayPerson {
  @Input() person: {name:string};
  @Input() address: {city:string, street:string};
  zipCode: string; // this is not an input, this is local state of this component

  constructor(private zipCodes: ZipCodes) {}

  onChanges(inputChanges) {
    if (inputChanges.address) { // this means that the address object was replaced
      this.zipCode = this.zipCodes(this.address);
    }
  }
}

Если объект адреса изменчив, другой компонент может обновить свойство улицы, не создавая новый объект адреса. Если это произойдет, ловушка onChanges не будет вызываться, и будет отображаться неправильный почтовый индекс. Есть, конечно, способы обойти это. Но проблема остается — мы должны быть глубоко осведомлены о том, как и когда объект адреса может измениться.

Производительность

Давайте вернемся к этому компоненту.

@Component({
  selector: 'person',
  template: `{{person.name}} lives at {{address.street}}, {{address.city}}`
})
class DisplayPerson {
  @Input() person: {name:string};
  @Input() address: {city:string, street:string};
}

От чего зависит этот компонент? Ну, это зависит от человека и адреса объектов. Но если эти объекты изменчивы, это не так много говорит. Поскольку они могут быть обновлены любым компонентом или службой в любое время, на компонент DisplayPerson может влиять любой другой компонент или служба. Вот почему по умолчанию Angular не делает никаких предположений о том, от чего зависит компонент. Поэтому он должен быть консервативным и проверять шаблон DisplayPerson при каждом событии браузера. Поскольку фреймворк должен делать это для каждого компонента, это может стать проблемой производительности.

Две проблемы с изменяемыми объектами

Подводя итог, это проблемы с изменяемыми объектами домена:

  • Они затрудняют отслеживание изменений как для разработчика, так и для фреймворка.
  • Они заставляют Angular быть консервативным по умолчанию, что негативно влияет на производительность.

Использование неизменяемых объектов

Моделирование состояния приложения с использованием неизменяемых объектов решает эти проблемы. Если оба personи addressявляются неизменяемыми, мы можем рассказать гораздо больше о том, когда компонент DisplayPerson может измениться. Компонент может измениться, если и только если любой из его входов изменяется. И мы можем сообщить об этом Angular, установив стратегию обнаружения изменений на OnPush.

@Component({
  selector: 'person',
  template: `{{person.name}} lives at {{address.street}}, {{address.city}} (zipCode)`,
  changeDetection: ChangeDetectionStrategy.OnPush // ⇐===
})
class DisplayPerson {
  @Input() person: {name:string};
  @Input() address: {city:string, street:string};
  zipCode: string;

  constructor(private zipCodes:ZipCodes) {}

  onChanges(inputChanges) {
    if (inputChanges.address) { // this means that the address was replaced
      this.zipCode = this.zipCodes(this.address);
    }
  }
}

Используя это изменения обнаружения ограничивает стратегию , когда Угловые должно проверить наличие обновлений «любое время что — то могущество изменения» до «только тогда , когда входы этого компонента были изменены». В результате платформа может быть намного более эффективной в обнаружении изменений в DisplayPerson. Если входные данные не изменяются, нет необходимости проверять шаблон компонента. Отлично! Неизменные объекты потрясающие!

Как использовать неизменяемые объекты в JS

Примитивные типы, такие как строки и числа, являются неизменными. Вы можете заморозить объекты и массивы JavaScript, чтобы сделать их неизменяемыми, но лучшим вариантом будет использование библиотек, таких как Immutable.js или Mori. Они обеспечивают эффективную реализацию неизменяемых коллекций и записей и просты в использовании.

Неизменность против инкапсуляции

Как обычно, есть компромиссы.

Моделирование состояния приложения с использованием исключительно неизменяемых объектов требует выталкивания управления состоянием из дерева компонентов. Думаю об этом. Так как addressявляется неизменным, мы не можем обновить его свойство улицы на месте. Вместо этого мы должны создать новый адресный объект. Скажем, этот адрес хранится в каком-то объекте PersonRecord. Поскольку этот объект также является неизменяемым, нам нужно будет создать новый объект PersonRecord. Следовательно, все состояние приложения должно быть обновлено, если мы изменим свойство улицы по адресу.

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

Поэтому мы решили удалить все локальные изменяемые состояния из наших компонентов, но так как состояние существует по причине, мы не можем просто удалить его — нам нужно переместить его куда-нибудь. Единственное место, куда его можно переместить — это объект состояния приложения. Это означает, что некоторое состояние, которое ранее было бы инкапсулировано, становится общедоступным. Давайте посмотрим на пример, иллюстрирующий это.

Скажем, у нас есть компонент typeahead, который мы можем использовать следующим образом:

<typeahead [options]="listOfOptions" [initValue]="value" (change)="onChange($event.selectedOption)">

И это эскиз класса компонента:

@Component({selector: 'typeahead', templateUrl: 'typeahead.html'})
class Typeahead {
  @Input() options: string[];
  @Input() initOption: string;
  @Output() change = new EventEmitter();
}

Это просто публичный интерфейс этого компонента. На практике у компонента typeahead будет намного больше происходящего. Например, он может хранить некоторую позицию прокрутки, которая может быть обновлена ​​только событиями, инициированными в typeahead.html:

@Component({selector: 'typeahead', templateUrl: 'typeahead.html'})
class Typeahead {
  @Input() options: string[];
  @Input() initOption: string;
  @Output() change = new EventEmitter();

  scrollingPosition: number;
}

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

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

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

Получение лучшего из обоих миров

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

  • Состояние приложения, которое передается, моделируется с использованием неизменяемых объектов.
  • Компоненты могут иметь локальное состояние, которое может обновляться только при изменении их входных данных или при возникновении события в их шаблонах.

Таким образом, мы допускаем изменяемое состояние, но в очень ограниченной области. Давайте посмотрим, как эта установка сравнивается с предыдущей.

Отслеживание изменений

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

Производительность

Эта настройка работает для нас, но она также работает для Angular. Фреймворк будет проверять компоненты OnPush только тогда, когда их входные данные изменяются или шаблоны компонентов генерируют события.

Инкапсуляция

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

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

Будь прагматичным

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