Подробное руководство по ручному управлению обнаружением изменений в Angular

AngularInDepth уходит от Medium. Эта статья, ее обновления и более свежие статьи размещены на новой платформе inDepth.dev

Этот пост является ответом на твит Шая. Он спрашивает, имеет ли смысл использовать NgDoCheck ловушку жизненного цикла для ручного сравнения значений вместо использования рекомендованного подхода с async конвейером. Это очень хороший вопрос, который требует глубокого понимания того, как все работает под капотом: обнаружение изменений, конвейеры и хуки жизненного цикла. Вот тут я и пришел 😎.

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

Я работаю адвокатом разработчиков в ag-Grid. Если вам интересно узнать о сетках данных или вы ищете идеальное решение для сетки данных Angular, попробуйте его с помощью руководства « Начать работу с сеткой Angular за 5 минут ». Я с радостью отвечу на любые ваши вопросы. И следите за мной, чтобы оставаться в курсе!

Давайте начнем!

Компоненты OnPush

В Angular у нас есть очень распространенный метод оптимизации, который требует добавления ChangeDetectionStrategy.OnPush в декоратор компонента. Предположим, у нас есть простая иерархия из двух таких компонентов:

При такой настройке Angular всегда запускает обнаружение изменений для компонентов A и B каждый раз. Если мы теперь добавим стратегию OnPush для компонента B:

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

Запуск обнаружения изменений вручную

Есть ли способ принудительно обнаружить изменения в компоненте B? Да, мы можем внедрить changeDetectorRef и использовать его метод markForCheck, чтобы указать для Angular, что этот компонент необходимо проверить. И поскольку ловушка NgDoCheck все равно будет срабатывать для компонента B, именно здесь мы должны вызвать метод:

Теперь компонент B всегда будет проверяться, когда Angular проверяет родительский компонент A. Давайте теперь посмотрим, где мы можем его использовать.

Входные привязки

Я говорил вам, что Angular запускает обнаружение изменений только для OnPush компонентов при изменении привязок. Итак, давайте посмотрим на пример с привязками ввода. Предположим, у нас есть объект, который передается от родительского компонента через входы:

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

Если вы сейчас запустите этот пример, после первого обнаружения изменения вы увидите напечатанное имя пользователя:

User name: A

Но когда мы нажимаем на кнопку и меняем имя в обратном вызове:

changeName() {
    this.user.name = 'B';
}

имя не обновляется на экране. И мы знаем почему. Это потому, что Angular выполняет поверхностное сравнение входных параметров, а ссылка на объект user не изменилась. Итак, как мы можем это исправить?

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

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

Асинхронные обновления

Теперь давайте немного усложним наш пример. Мы собираемся представить службу на основе RxJs, которая передает обновления асинхронно. Это похоже на то, что у вас есть в архитектурах на основе NgRx. Я собираюсь использовать BehaviorSubject в качестве источника значений, потому что мне нужно запустить поток с начальным значением:

Итак, мы получаем этот поток user объектов в дочернем компоненте. Нам нужно подписаться на поток и проверить, обновляются ли значения. И общий подход к этому - использование Async pipe.

Асинхронный канал

Итак, вот реализация дочернего B компонента:

Вот демо. Но есть ли другой способ не использовать трубку?

Ручная проверка и обнаружение изменений

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

Вы можете поиграть с этим здесь.

Однако в идеале мы хотели бы переместить нашу логику сравнения и обновления из NgDoCheck и поместить ее в обратный вызов подписки, потому что именно тогда будет доступно новое значение:

Поиграй с этим здесь.

Что интересно, это именно то, что делает под капотом асинхронный конвейер:

Итак, какое решение быстрее?

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

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

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

Как видите, код асинхронного канала вызывает метод transform в экземпляре канала для получения нового значения. Канал вернет последнее значение, полученное по подписке.

Сравните его с простым кодом, созданным для ручного подхода:

Это функции, выполняемые Angular при проверке B компонента.

Еще несколько интересных вещей

В отличие от привязок ввода, которые выполняют поверхностное сравнение, реализация асинхронного конвейера не выполняет сравнение вообще (спасибо Олене Хорал за то, что это заметила). Он обрабатывает каждое новое излучение как обновление, даже если оно соответствует ранее выданному значению. Вот реализация родительского компонента A, который излучает тот же объект. Несмотря на это, Angular по-прежнему выполняет обнаружение изменений для компонента B:

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

Где это актуально? Что ж, в нашем случае нас интересует только свойство name из объекта user, потому что мы используем его в шаблоне. На самом деле нас не волнует весь объект и тот факт, что ссылка на объект может измениться. Если имя то же самое, нам не нужно повторно визуализировать компонент. Но с асинхронным конвейером этого не избежать.

NgDoCheck сам по себе не без проблем :) Поскольку ловушка срабатывает только в том случае, если родительский компонент отмечен, он не сработает, если один из его родительских компонентов использует стратегию OnPush и не проверяется во время обнаружения изменений. Таким образом, вы не можете полагаться на него, чтобы инициировать обнаружение изменений, когда вы получаете новое значение через службу. В этом случае решение, которое я показал, добавив markForCheck в обратный вызов подписки, является правильным решением.

Заключение

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

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

Хотите узнать больше об обнаружении изменений в Angular?

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

Чтобы узнать больше, подпишитесь на меня в Twitter и Medium.