Основні відмінності між конструктором і ngOnInit в роботі з компонентами Angular

15

Від автора: один з найбільш частих питань про Angular компоненти на stackoverflow – чим відрізняється конструктор від ngOnInit. У даного питання більше 100к переглядів. Я дав там свою відповідь і вирішив розвинути його в цю статтю. Майже всі відповіді в тред і статті в мережі зосереджені на відмінності у використанні. Я ж дам більш розгорнуте порівняння, що стосується процесу ініціалізації компонентів.

Основні відмінності між конструктором і ngOnInit в роботі з компонентами Angular

Відмінності в JS/TS мовою

Почнемо з самого очевидного відмінності – відмінності мов. ngOnInit – це лише метод класу, структурно не відрізняється від інших методів класу. Команда Angular вирішила його так назвати, але у нього могло бути зовсім іншу назву:

class MyComponent {
ngOnInit() { }
otherNameForNgOnInit() { }
}

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

export const enum NodeFlags {

OnInit = 1 << 16,

За допомогою цього прапора потім вирішується, викликати метод на об’єкті компонента під час виявлення змін:

if (def.flags & NodeFlags.OnInit && …) {
componentClassInstance.ngOnInit();
}

Конструктор, у свою чергу, зовсім інше. Незважаючи на те, визначили його або немає в класі TypeScript, він все одно буде викликатися при створенні об’єкта класу. Це відбувається тому, що конструктор класу typescript переводиться в JavaScript конструктор:

class MyComponent {
constructor() {
console.log(‘Hello’);
}
}

Переводиться в:

function MyComponent() {
console.log(‘Hello’);
}

Щоб створити об’єкт класу, цю функцію необхідно викликати з оператором new:

const componentInstance = new MyComponent(

Якщо не створювати конструктор в класі, він перетвориться на порожню функцію:

class MyComponent { }

Переводиться в порожню функцію:

function MyComponent() {}

Ось чому я кажу, що конструктор викликається незалежно від того, чи оголошено його у класі чи ні.

Відмінності в процесі ініціалізації компонентів

Між двома функціями є величезна відмінність у плані ініціалізації компонентів. Процес початкового завантаження Angular складається з двох основних етапів:

Створення дерева компонентів

Запуск виявлення змін

Конструктор компонента викликається, коли Angular створює дерево компонентів. Всі хуки життєвого циклу, в тому числі і ngOnInit, викликаються як частину наступної фази виявлення змін. Як правило, логіка ініціалізації компонента вимагає DI провайдери або доступні вхідні призначення, або отрендеренный DOM. Все це доступно на різних етапах процесу початкової завантаження Angular.

Коли Angular створює дерево компонентів, ін’єктор кореневого модуля вже налаштований, тобто ви можете вставляти будь-які глобальні залежності. Коли Angular ініціалізує клас дочірнього компонента, ін’єктор батьківського компонента вже налаштований. Тобто ви можете вставляти провайдери, визначені по батьківського компоненту, включаючи сам батьківський компонент. Конструктор компонента лише метод, що викликається в контексті ін’єктора. Якщо вам потрібні залежності, отримати їх можна тільки звідси. Механізм комунікації @input обробляється як частину наступної фази виявлення змін, тому призначення вхідних даних не доступно в конструкторі.

Коли Angular запускає виявлення змін, дерево компонентів вже побудовано, і конструктори для всіх компонентів в дереві викликані. На даному етапі всі вузли шаблонів компонентів додаються в DOM. Тут доступна вся інформація, яка може знадобитися для ініціалізації компонента – DI провайдери, DOM і призначення введення.

Більш детально про виявлення змін можете прочитати в статті «все що потрібно знати про виявлення змін у Angular», а про те, як Angular обробляє вхідні дані у статті «механіка оновлення призначень властивостей Angular».

Продемонструємо ці фази невеликим прикладом. Припустимо, у вас наступний шаблон:

Angular починає попередню завантаження додатка. Як сказано вище, спочатку він створює класи для всіх компонентів. Тобто викликається конструктор MyAppComponent. При виконанні конструктора компонента Angular дозволяє всі залежності, встановлені в конструктор MyAppComponent, і передає їх у вигляді параметрів. Також створюється вузол DOM, який буде зберігати компонент my-app. Далі створюється хост елемент для child-comp, і викликається конструктор ChildComponent. На цьому етапі Angular не пов’язаний з прив’язкою вхідних даних та хуками життєвого циклу. Тобто після завершення процесу Angular має наступне дерево компонентів:

MyAppView
— MyApp component instance
— my-app host data element
ChildComponentView
— ChildComponent component instance
— child-comp host data element

Тільки після цього Angular запускає виявлення змін і оновлює призначення для my-app і викликає ngOnInit на об’єкті MyAppComponent. Далі відбувається перехід до оновлення призначень для child-comp і викликом ngOnInit на класі ChildComponent.

Більш детально про вигляді, про який я говорив вище, можна прочитати в статті «ось чому ви не знайдете компоненти в Angular».

Відмінності у використанні

Давайте розберемо, в чому відмінність з точки зору використання.

Конструктор

Конструктор класу в Angular майже завжди використовується для вставки залежностей. Angular викликає constructor injection pattern, який детально розбирався тут. Більш детально про його архітектурі можна прочитати в статті «ін’єкція в конструкторі або ін’єкція в сеттере» від Miško Hevery.

Тим не менше, використання конструктора не обмежена DI. Наприклад, директива router-outlet модуля @angular/router використовує його для своєї реєстрації та визначення свого положення (viewContainerRef) всередині екосистеми роутера. Я описував цей підхід у статті «от як можна отримати ViewContainerRef до обробки запиту @ViewChild».

Намагайтеся писати якомога менше логіки в конструкторі.

NgOnInit

Як ми вже знаємо, коли Angular викликає ngOnInit, він вже побудував DOM компонента, вставив всі необхідні залежності через конструктор і обробив призначення вхідних даних. Тут зберігається все необхідна інформація, що робить цей метод підходящим місцем для логіки ініціалізації.

Намагайтеся використовувати ngOnInit для виконання логіки ініціалізації, навіть якщо ця логіка не залежить від DI, DOM і призначень входу.