AngularInDepth уходит от Medium. Более свежие статьи размещаются на новой платформе inDepth.dev. Спасибо за то, что участвуете в глубоком движении!

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

Давайте посмотрим на этот пример:

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

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

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

Из статьи мы можем узнать, что Angular оптимизирует чистые каналы, создавая только один экземпляр канала, независимо от того, сколько каналов фактически используется в шаблоне. Однако большее улучшение производительности происходит из-за того, что Angular оптимизирует чистые каналы, вызывая transform только при изменении параметров функции.

Возвращаясь к нашему примеру, мы можем улучшить его, переместив логику из функции square в чистый канал, как показано ниже:

и используйте pipe следующим образом:

Эффект можно увидеть здесь. Этот подход хорошо описан Минко Гечевым в своей статье о том, как сделать приложения Angular быстрее. Однако превращение каждой функции в конвейер может оказаться непосильным. По крайней мере, из-за шаблонного кода мы должны писать или генерировать с помощью CLI.

Итак, есть встроенный механизм для мемоизации в каналах Angular, и чтобы использовать его, мы должны обернуть наши функции в каналы?

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

Управление трубами под капотом

Перед чтением следующего раздела я настоятельно рекомендую прочитать Механику обновления DOM в Angular Макса Корецкого, aka Wizard, потому что для всех типов выражений большая часть логики разделяется.

Компилятор разбивает конвейерное выражение на несколько значений:

+------------------+------------------------------------------+
|                  |            Description                   |
+------------------+------------------------------------------+
| instance of the  |                                          |
| pipe             |                                          |
+------------------+------------------------------------------+
| piped value      | the one you put to the left of the pipe  |
|                  | in template expression                   |
+------------------+------------------------------------------+
| additional       | which are separated by semicolon    |
| arguments        | in template expression                   |
+------------------+------------------------------------------+

Для каждой чистой трубы Angular создает один экземпляр трубы в виде узла просмотра. Вот соответствующий фрагмент из скомпилированного кода функции updateRenderer:

_ck обозначает функцию prodCheckAndUpdateNode.
_v обозначает просмотр.
Число 4 обозначает индекс узла.
0 - параметр argStyle, не важный в контексте этой статьи.
jit_nodeValue_8(_v, 0) возвращает экземпляр канала.
_co - это экземпляр компонента, а _co.counter - передаваемое значение.

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

В этой функции:

+------------------+-----------------------------------+
|       Name       |            Description            |
+------------------+-----------------------------------+
| v0               | holds instance of the pipe        |
+------------------+-----------------------------------+
| v1               | value which is going to be piped  |
+------------------+-----------------------------------+
| v2…v9            | can contain additional parameters |
|                  | passed to pipe in template        |
+------------------+-----------------------------------+

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

Фактическая грязная проверка, имитирующая мемоизацию, происходит в функции checkAndUpdateBinding:

Как вы видите, в строке 14 oldValue по соответствующему индексу сравнивается с новым значением. Если обнаруживается неравенство, oldValue перезаписывается (строка №4), и функция перестает возвращать истину.

Только после этого, когда Angular узнает, произошло ли изменение одного из значений, он готов вызвать метод transform канала (строка № 21 предыдущей сути ). Зная, что первое значение на самом деле является каналом, v0 переназначается константе pipe (строка № 18). Затем в строке 20 в конечном итоге вызывается pipe#transform.

После этого значение выражения обновляется результатом вызова pipe # transform (строка № 30).

Но что будет, если труба не чистая?

Что ж, вот соответствующий код из функции updateRenderer:

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

Что мы только что узнали?

Мемоизация - это не часть программы.

Мемоизация встроена в механизм обнаружения изменений, и флаг pure , который вы установили на true в декораторе конвейера, является просто подсказкой для детектора изменений, чтобы соответствующим образом управлять вашим конвейером. Как вы знаете, мы можем внедрять каналы как службы и использовать их в коде TS. Но таким образом мы теряем их чистоту. Их метод преобразования будет вызываться как простые функции без мемоизации.

А как насчет универсальной трубки мемоизации?

Как мы видели выше, детектор изменений Angular проверяет любое значение из конвейерного выражения на наличие изменений. Оба значения, которые мы на самом деле pipe с | оператор и дополнительные параметры, которые принимает функция transform. Если какое-либо из этих значений изменяется, срабатывает transform функция трубы.

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

Должно получиться так:

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

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

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

Вместо прямого вызова функции square вы передаете ее memoize pipe вместе с аргументом, требуемым функцией square.

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

Заключение

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

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

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

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

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