Основні відмінності між чистими і брудними pipes в Angular і чому це так важливо

17

Від автора: основні відмінності між чистими і брудними pipes в Angular і чому це так важливо — про це йде мова в даній статті.

Основні відмінності між чистими і брудними pipes в Angular і чому це так важливо

При написанні користувальницького пайпа можна вказати, яким він буде: чистим або брудним:

@Pipe({
name: ‘myCustomPipe’,
pure: false/true <—— here (default is `true`)
})
export class MyCustomPipe {}

Angular має досить хорошу документацію по пайпам. Знайти її можна за посиланням. Але як часто буває в документаціях, в ній відсутній чіткий поділ. У цій статті я заповню прогалини і продемонструю вам різницю з точки зору функціонального програмування, яке показує, звідки пішла ідея чистих і брудних Angular pipes. Крім відмінностей ви дізнаєтеся, як різні пайпи впливають на продуктивність, а це вже допоможе вам писати ефективні і продуктивні пайпи.

Чиста функція

В мережі повно інформації з функціонального програмування, і майже кожен розробник знає, що таке чиста функція. Особисто я визначаю її, як функцію, що не має внутрішнього стану. Це означає, що всі виконувані операції не залежать від цього стану і ставлять одні і ті ж вхідні параметри, а також повертають одне і те ж задане значення.

Нижче представлено 2 версії функції, яка складає числа. Перша чиста, друга брудна:

const addPure = (v1, v2) => {
return v1 + v2;
};
const addImpure = (() => {
let state = 0;
return (v) => {
return state += v;
}
})();

Якщо викликати обидві функції з однаковими вхідними параметрами, наприклад, число 1, перша буде давати однаковий результат при кожному виклику:

addPure(1, 1); // 2
addPure(1, 1); // 2
addPure(1, 1); // 2

А друга буде давати різний результат:

addImpure(1); // 1
addImpure(1); // 2
addImpure(1); // 3

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

Розберемо ще одне цікаве наслідок того, що у функції є стан. Скажімо, у вас є об’єкт calculator, що приймає в якості параметра функцію з додаванням чисел і використовує її для обчислень:

class Calculator {
constructor(addFn) {
this.addFn = addFn;
}
add(v1, v2) {
return this.addFn(v1, v2);
}
}

Якщо функція чиста і не має стану, її можна вільно поширювати на безліч об’єктів класу Calculator:

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:

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 пайп і зробили його чистим:

@Pipe({
name: ‘myCustomPipe’,
pure: true
})
export class MyCustomPipe {}

І використовуємо його таким чином в шаблоні компонента:

{{v1 | customPipe}}
{{v2 | customPipe}}

Пайп чистий, це означає, що у нього немає внутрішнього стану, і їм можна ділитися. Як зробити це в Angular? Незважаючи на 2 записи в шаблоні, Angular може створити тільки один об’єкт пайпа, який можна ділити між використаннями. Хто читав мої попередні статті «що таке component factory», знає, що нижче показаний скомпільований код, що визначає один пайп:

function View_AppComponent_0(_l) {
return viewDef_1(0, [
pipeDef_2(0, ExponentialStrengthPipe_3, []), // node index 0

Який поширюється в функції updateRenderer:

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.

Проте якщо зробити наш пайп брудним і додати в нього якесь внутрішнє стан:

@Pipe({
name: ‘myCustomPipe’,
pure: false
})
export class MyCustomPipe {}

Ми не хочемо, щоб перший виклик пайпа вплинув на другий, тому Angular створює 2 об’єкта пайпа, кожен зі своїм станом:

function View_AppComponent_0(_l) {
return viewDef(0, [

pipeDef_2(0, ExponentialStrengthPipe, []) // node index 4

pipeDef_2(0, ExponentialStrengthPipe, []) // node index 8

І він не поширюється функції updateRenderer:

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 вхідні параметри передаються в пайп наступним чином:

{{v1 | customPipe:param1:param2}}

Якщо пайп чистий, ми точно знаємо, що його вихід (через метод transform) строго задається його вхідними параметрами. Якщо вхід не змінюється, вихід також не змінюється. Такий підхід дозволяє Angular оптимізувати пайп і викликати метод transform тільки при зміні вхідних параметрів.

Однак якщо пайп брудний і має внутрішній стан, то ті ж самі параметри не гарантують нам той самий вихід, що вже було показано на приклад брудної функції addFn в першій главі. Тобто Angular змушений викликати transform функцію на об’єкт пайпа при кожному дайджесті.

AsyncPipe з пакету @angular/common відмінний приклад брудного пайпа. У нього є внутрішній стан, який містить базову підписку, створену шляхом підписки на вами, переданий в пайп в якості параметра. Тому Angular необхідно створити новий об’єкт для кожного виклику пайпа, щоб різні observables не впливали один на одного. Також йому необхідно викликати метод transform на кожен дайджест, так як навіть якщо параметр вами може не змінюватися, через цей вами може приїхати нове значення, яке необхідно обробити через виявлення змін.

Ще два брудних пайпа JsonPipe і SlicePipe. Обидва пайпа необхідно переоцінювати на кожен дайджест, так як у них є внутрішній стан, який зберігає об’єкти, які можуть мутувати без зміни посилань на об’єкти (параметр пайпа не змінюється).

Інші стандартні пайпи в Angular чисті.

Висновок

Як ми побачили, брудні пайпи можуть надати значний вплив на продуктивність, якщо з ними неакуратно звертатися. Зниження продуктивності може бути викликано тим, що Angular створює кілька об’єктів на брудний пайп і викликає метод transform на кожен цикл дайджесту.

Сподіваюся, після статті ви тепер знаєте відмінності цих двох типів, як Angular обробляє їх, а також яку модель використовувати при проектуванні і реалізації кастомного пайпа.