Виктор Савкин - соучредитель nrwl.io. Ранее он входил в основную команду Angular в Google и создавал модули внедрения зависимостей, обнаружения изменений, форм и маршрутизатора

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

Прочитать серию

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

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

Пример приложения

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

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

Две фазы

Angular разделяет обновление модели приложения и отражение состояния модели в представлении на две отдельные фазы. Разработчик несет ответственность за обновление модели приложения. Angular через привязки, посредством обнаружения изменений, отвечает за отображение состояния модели в представлении. Фреймворк делает это автоматически при каждом повороте виртуальной машины.

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

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

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

Затем определите модель приложения, которая будет хранить состояние нашего приложения.

А теперь представьте событие, изменяющее модель. Допустим, я смотрел лекцию «Мы уже на месте», она мне очень понравилась, и я решил поставить 9.9.

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

В этом примере мы не изменяем разговор, а вместо этого создаем новый массив новых разговоров каждый раз, когда происходит изменение, что приводит к нескольким хорошим свойствам. Но стоит отметить, что Angular не требует от нас использования неизменяемых объектов, и мы могли бы так же легко написать что-нибудь вроде `talk.rating = newRating`.

Хорошо, после выполнения `rateTalk` обновленная модель будет выглядеть так:

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

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

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

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

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

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

Почему?

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

Предсказуемость

Во-первых, использование обнаружения изменений только для обновления состояния представления ограничивает количество мест, в которых модель приложения может быть изменена. В этом примере это может произойти только в функции `rateTalk`. Наблюдатель не может обновить его «автоматически». Это упрощает обеспечение инвариантов, что упрощает устранение неполадок и рефакторинг кода.

Во-вторых, это помогает нам понять распространение состояния представления. Подумайте, что мы можем сказать о компоненте разговора, просто рассматривая его изолированно. Поскольку мы используем неизменяемые данные, мы знаем, что до тех пор, пока мы не используем talk = в компоненте Talk, единственный способ изменить то, что отображает компонент, - это обновить ввод. Это надежные гарантии, которые позволяют нам думать об этом компоненте изолированно.

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

Начиная с Angular 2.x становится легче думать о компонентах, потому что структура ограничивает количество способов, которыми она может изменять компоненты, и эти изменения предсказуемы.

Представление

Основное преимущество разделения состоит в том, что оно позволяет нам ограничивать распространение состояния представления. Это делает систему более предсказуемой, но также и более производительной. Например, тот факт, что граф обнаружения изменений в Angular можно смоделировать в виде дерева, позволил нам избавиться от дайджеста TTL (несколько дайджестов запускаются до тех пор, пока не перестанут происходить изменения). Теперь система стабилизируется после одного прохода.

Как Angular это обеспечивает?

Что произойдет, если я попытаюсь нарушить разделение? Что, если я попытаюсь изменить модель приложения внутри установщика, который запускается системой обнаружения изменений?

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

Контент и просмотр детей

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

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

Дочерними элементами содержимого компонента вкладок являются три компонента вкладок. Их предоставил пользователь компонента tabs. Предыдущая и следующая кнопки являются дочерними элементами представления компонента вкладок. Таковые предусмотрены автором компонента tabs.

Это порядок, в котором Angular будет проверять привязки:

  • Сначала он проверит привязки компонента вкладок, которых нет.
  • Он проверит три компонента вкладок, дочерние элементы содержимого компонента вкладок.
  • Он проверит шаблон компонента вкладок.

ChangeDetectionStrategy.OnPush

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

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

Использование этой стратегии обнаружения изменений ограничивает, когда Angular должен проверять наличие обновлений с «в любой момент, когда что-то может измениться» до «только тогда, когда изменились входные данные этого компонента». В результате фреймворк может намного более эффективно обнаруживать изменения в TalkCmp. Если никакие входные данные не меняются, нет необходимости проверять шаблон компонента. В дополнение к зависимости от неизменяемых входов компоненты OnPush также могут иметь локальное изменяемое состояние.

Подведем итоги

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

Основная книга Angular

Эта статья основана на книге Essential Angular, которую вы можете найти здесь https://leanpub.com/essential_angular. Если вам понравилась статья, загляните в книгу!

Виктор Савкин - соучредитель Nrwl. Мы помогаем компаниям развиваться, как Google, с 2016 года. Мы предоставляем консультации, инжиниринг и инструменты.

Если вам это понравилось, нажмите 👏 ниже, чтобы другие люди увидели это здесь, на Medium. Подпишитесь на @victorsavkin, чтобы узнать больше о монорепозиториях, Nx, Angular и React.