От автора: основные различия между чистыми и грязными pipes в Angular и почему это так важно — об этом идет речь в данной статье.
При написании пользовательского пайпа можно указать, каким он будет: чистым или грязным:
1 2 3 4 5 |
@Pipe({ name: 'myCustomPipe', pure: false/true <----- here (default is `true`) }) export class MyCustomPipe {} |
Angular имеет довольно хорошую документацию по пайпам. Найти ее можно по ссылке. Но как часто бывает в документациях, в ней отсутствует четкое разделение. В этой статье я заполню пробелы и продемонстрирую вам разницу с точки зрения функционального программирования, которое показывает, откуда пошла идея чистых и грязных Angular pipes. Помимо различий вы узнаете, как разные пайпы влияют на производительность, а это уже поможет вам писать эффективные и производительные пайпы.
Чистая функция
В сети полно информации по функциональному программированию, и почти каждый разработчик знает, что такое чистая функция. Лично я определяю ее, как функцию, не имеющую внутреннего состояния. Это значит, что все выполняемые операции не зависят от этого состояния и задают одни и те же входные параметры, а также возвращают одно и то же заданное значение.
Ниже представлено 2 версии функции, которая складывает числа. Первая чистая, вторая грязная:
1 2 3 4 5 6 7 8 9 10 |
const addPure = (v1, v2) => { return v1 + v2; }; const addImpure = (() => { let state = 0; return (v) => { return state += v; } })(); |
Если вызвать обе функции с одинаковыми входными параметрами, например, число 1, первая будет давать одинаковый результат при каждом вызове:
1 2 3 |
addPure(1, 1); // 2 addPure(1, 1); // 2 addPure(1, 1); // 2 |
А вторая будет давать разный результат:
1 2 3 |
addImpure(1); // 1 addImpure(1); // 2 addImpure(1); // 3 |
Основная идея тут в том, что даже если входные данные не меняются, грязная функция может давать разный результат. То есть мы не можем с помощью входных значений определить, изменится ли результат или нет.
Разберем еще одно интересное следствие того, что у функции есть состояние. Скажем, у вас есть объект calculator, который принимает в качестве параметра функцию по сложению чисел и использует ее для вычислений:
1 2 3 4 5 6 7 8 9 |
class Calculator { constructor(addFn) { this.addFn = addFn; } add(v1, v2) { return this.addFn(v1, v2); } } |
Если функция чистая и не имеет состояния, ее можно свободно распространять на множество объектов класса Calculator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class Calculator { constructor(addFn) { this.addFn = addFn; } add(v1, v2) { return this.addFn(v1, v2); } } const c1 = new Calculator(add); const c2 = new Calculator(add); c1.add(1, 1); // 2 c2.add(1, 1); // 2 |
Грязную функцию нельзя распространять. Потому что операции, выполняемые одним объектом Calculator будут влиять на состояние функции и, следовательно, на результат операций другого объекта Calculator:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
const add = (() => { let state = 0; return (v) => { return state += v; } })(); class Calculator { constructor(addFn) { this.addFn = addFn; } add(v1, v2) { return (this.addFn(v1), this.addFn(v2)); } } const c1 = new Calculator(add); const c2 = new Calculator(add); c1.add(1, 1); // 2 c2.add(1, 1); // 4 <------ here we have `4` instead of `2` |
Посмотрите, первый вызов метода add у второго объекта возвращает не 2, а 4. Давайте вспомним, что мы узнали о функциях. Чистые:
Значения входных параметров определяют результат, и если входные параметры не меняются, то и результат не изменится
Их можно распространять на множество объектов, не влияя на результат
Грязные:
С помощью входных значений нельзя определить, изменится выход или нет
Нельзя распространять, так как внутреннее состояние может быть изменено снаружи
Применение знаний к пайпам Angular
Предположим, мы создали 1 пайп и сделали его чистым:
1 2 3 4 5 |
@Pipe({ name: 'myCustomPipe', pure: true }) export class MyCustomPipe {} |
И используем его следующим образом в шаблоне компонента:
1 2 |
<span>{{v1 | customPipe}}</span> <span>{{v2 | customPipe}}</span> |
Пайп чистый, это значит, что у него нет внутреннего состояния, и им можно делиться. Как сделать это в Angular? Несмотря на 2 записи в шаблоне, Angular может создать только один объект пайпа, который можно делить между использованиями. Кто читал мои предыдущие статьи «что такое component factory», знает, что ниже показан скомпилированный код, определяющий один пайп:
1 2 3 4 |
function View_AppComponent_0(_l) { return viewDef_1(0, [ pipeDef_2(0, ExponentialStrengthPipe_3, []), // node index 0 ... |
Который распространяется в функции updateRenderer:
1 2 3 4 5 |
function(_ck,_v) { unwrapValue_7(_v,4,0,_ck(_v,5,0,nodeValue_8(_v, 0),...); ^^^ unwrapValue_7(_v,8,0,_ck(_v,9,0,nodeValue_8(_v, 0),...); ^^^ |
С помощью функции unwrapValue вытягивается текущее значение пайпа с помощью вызова transform на нем. Объект пайпа ссылается по индексу узла в вызове функции nodeValue — в нашем случае это 0.
Однако если сделать наш пайп грязным и добавить в него некое внутреннее состояние:
1 2 3 4 5 |
@Pipe({ name: 'myCustomPipe', pure: false }) export class MyCustomPipe {} |
Мы не хотим, чтобы первый вызов пайпа повлиял на второй, поэтому Angular создает 2 объекта пайпа, каждый со своим состоянием:
1 2 3 4 5 6 |
function View_AppComponent_0(_l) { return viewDef(0, [ ... pipeDef_2(0, ExponentialStrengthPipe, []) // node index 4 ... pipeDef_2(0, ExponentialStrengthPipe, []) // node index 8 |
И он не распространяется в функции updateRenderer:
1 2 3 4 5 |
function(_ck,_v) { unwrapValue_7(_v,4,0,_ck(_v,5,0,nodeValue_8(_v, 4),...); ^^^ unwrapValue_7(_v,8,0,_ck(_v,9,0,nodeValue_8(_v, 8),...); ^^^ |
Посмотрите, теперь вместо узлового индекса 0 для каждого вызова Angular использует разные узловые индексы – 4 и 8 соответственно.
Второй вывод из первой главы: в чистых функциях мы с помощью входных значений можем определить, изменится выход или нет, а в грязных функциях мы этого не можем гарантировать. В Angular входные параметры передаются в пайп следующим образом:
1 |
<span>{{v1 | customPipe:param1:param2}}</span> |
Если пайп чистый, мы точно знаем, что его выход (через метод transform) строго задается его входными параметрами. Если вход не меняется, выход также не меняется. Такой подход позволяет Angular оптимизировать пайп и вызывать метод transform только при изменении входных параметров.
Однако если пайп грязный и имеет внутреннее состояние, то те же самые параметры не гарантируют нам тот же выход, что уже было показано на пример грязной функции addFn в первой главе. То есть Angular вынужден вызывать transform функцию на объект пайпа при каждом дайджесте.
AsyncPipe из пакета @angular/common отличный пример грязного пайпа. В него есть внутреннее состояние, которое содержит базовую подписку, созданную путем подписки на observable, переданный в пайп в качестве параметра. Поэтому Angular необходимо создать новый объект для каждого вызова пайпа, чтобы разные observables не влияли друг на друга. Также ему необходимо вызывать метод transform на каждый дайджест, так как даже если параметр observable может не меняться, через этот observable может приехать новое значение, которое необходимо обработать через обнаружение изменений.
Еще два грязных пайпа JsonPipe и SlicePipe. Оба пайпа необходимо переоценивать на каждый дайджест, так как у них есть внутреннее состояние, которое хранит объекты, которые могут мутировать без изменения ссылок на объекты (параметр пайпа не меняется).
Остальные стандартные пайпы в Angular чистые.
Заключение
Как мы увидели, грязные пайпы могут оказать значительное воздействие на производительность, если с ними неаккуратно обращаться. Снижение производительности может быть вызвано тем, что Angular создает несколько объектов на грязный пайп и вызывает метод transform на каждый цикл дайджеста.
Надеюсь, после статьи вы теперь знаете различия этих двух типов, как Angular обрабатывает их, а также какую модель использовать при проектировании и реализации кастомного пайпа.
Автор: Maxim Koretskyi
Источник: //blog.angularindepth.com
Редакция: Команда webformyself.