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

В этой статье я подробно расскажу о системе обнаружения изменений Angular 2.

Обзор высокого уровня

Приложение Angular 2 - это дерево компонентов.

Приложение Angular 2 - это реактивная система, в основе которой лежит обнаружение изменений.

Каждый компонент получает детектор изменений, отвечающий за проверку привязок, определенных в его шаблоне. Примеры привязок: {{todo.text}} и [todo] = "t". Детекторы изменений распространяют привязки от корня к листьям в порядке глубины.

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

Насколько это быстро?

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

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

Неизменяемые объекты

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

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

Реализовать это несложно. Просто установите для стратегии обнаружения изменений значение OnPush.

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

Наблюдаемые объекты

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

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

Позвольте мне набросать небольшой пример, демонстрирующий проблему.

Шаблон ObservableTodosCmp:

Наконец, ObservableTodoCmp:

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

Способ справиться с этим - проверить путь от корня к измененному компоненту Todo, когда его наблюдаемый todo запускает событие. Система обнаружения изменений позаботится об этом.

Скажем, наше приложение использует только наблюдаемые объекты. При загрузке Angular проверит все объекты.

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

Допустим, первая задача вызывает событие. Система перейдет в следующее состояние:

И после проверки App_ChangeDetector, Todos_ChangeDetector и первого Todo_ChangeDetector он вернется в это состояние.

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

Эта возможность не привязана к какой-либо конкретной библиотеке. Обучение Angular любой наблюдаемой библиотеке - это вопрос нескольких строк кода.

Вызывают ли наблюдаемые каскадные обновления?

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

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

Должен ли я везде использовать наблюдаемые / неизменяемые объекты, чтобы увидеть преимущества?

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

Резюме

  • Приложение Angular 2 - это реактивная система.
  • Система обнаружения изменений распространяет привязки от корня к листьям.
  • В отличие от Angular 1.x, граф обнаружения изменений представляет собой ориентированное дерево. В результате система более производительна и предсказуема.
  • По умолчанию система обнаружения изменений просматривает все дерево. Но если вы используете неизменяемые объекты или наблюдаемые, вы можете воспользоваться ими и проверять части дерева только в том случае, если они «действительно меняются».
  • Эти оптимизации составляют и не нарушают гарантий, предоставляемых обнаружением изменений.

Виктор Савкин - соучредитель Nrwl - Enterprise Angular Consulting.

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