Кутові гачки життєвого циклу: ngOnChanges, ngOnInit та багато іншого

Навіщо нам потрібні гачки життєвого циклу?

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

В одному випадку шаблон може бути готовим, в іншому дані будуть завершені. Кодування для кожного екземпляра вимагає засобів виявлення. Гаки життєвого циклу відповідають на цю потребу. Сучасні інтерфейсні фреймворки комплектуються різноманітними гачками життєвого циклу. Кутовий - не виняток

Пояснення гачків життєвого циклу

Гаки життєвого циклу - це приурочені методи. Вони різняться тим, коли і чому страчують. Виявлення змін викликає ці методи. Вони виконуються залежно від умов поточного циклу. Кутові прогони постійно виявляють зміни на своїх даних. Гаки життєвого циклу допомагають керувати його наслідками.

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

Деякі активи доступні лише після виконання певного хука. Звичайно, гачок виконується лише за певних умов, встановлених поточним циклом виявлення змін.

У цій статті представлені гачки життєвого циклу в порядку їх виконання (якщо всі вони виконуються). Деякі умови заслуговують активації гачка. Є кілька людей, які виконують лише один раз після ініціалізації компонента.

Всі методи життєвого циклу доступні в @angular/core. Хоча це не потрібно, Angular рекомендує застосовувати кожен гачок. Ця практика призводить до кращих повідомлень про помилки щодо компонента.

Наказ про виконання гачків життєвого циклу

ngOnChanges

ngOnChangesтригери після модифікації @Inputпов'язаних членів класу. Дані, пов’язані @Input()декоратором, надходять із зовнішнього джерела. Коли зовнішнє джерело змінює ці дані у виявляється спосіб, воно знову проходить через @Inputвластивість.

За допомогою цього оновлення ngOnChangesнегайно запускається. Він також спрацьовує при ініціалізації вхідних даних. Гак отримує один необов’язковий параметр типу SimpleChanges. Це значення містить інформацію про змінені властивості, пов'язані з введенням.

import { Component, Input, OnChanges } from '@angular/core'; @Component({ selector: 'app-child', template: ` 

Child Component

TICKS: {{ lifecycleTicks }}

DATA: {{ data }}

` }) export class ChildComponent implements OnChanges { @Input() data: string; lifecycleTicks: number = 0; ngOnChanges() { this.lifecycleTicks++; } } @Component({ selector: 'app-parent', template: `

ngOnChanges Example

` }) export class ParentComponent { arbitraryData: string = 'initial'; constructor() { setTimeout(() => { this.arbitraryData = 'final'; }, 5000); } }

Короткий зміст: ParentComponent прив'язує вхідні дані до ChildComponent. Компонент отримує ці дані через свою@Inputвластивість. ngOnChangesпожежі. Через п’ять секундsetTimeoutспрацьовує зворотний дзвінок. ParentComponent мутує джерело даних властивості ChildComponent, пов’язаної із введенням. Нові дані протікають через властивість введення. ngOnChangesпожежі ще раз.

ngOnInit

ngOnInitспрацьовує один раз після ініціалізації властивостей компонента input-bound ( @Input). Наступний приклад буде виглядати подібно до останнього. Гак не спрацьовує, оскільки ChildComponent отримує вхідні дані. Швидше, він спрацьовує відразу після того, як дані відтворюються у шаблоні ChildComponent.

import { Component, Input, OnInit } from '@angular/core'; @Component({ selector: 'app-child', template: ` 

Child Component

TICKS: {{ lifecycleTicks }}

DATA: {{ data }}

` }) export class ChildComponent implements OnInit { @Input() data: string; lifecycleTicks: number = 0; ngOnInit() { this.lifecycleTicks++; } } @Component({ selector: 'app-parent', template: `

ngOnInit Example

` }) export class ParentComponent { arbitraryData: string = 'initial'; constructor() { setTimeout(() => { this.arbitraryData = 'final'; }, 5000); } }

Короткий зміст: ParentComponent прив'язує вхідні дані до ChildComponent. ChildComponent отримує ці дані через свою@Inputвластивість. Дані відображаються в шаблоні. ngOnInitпожежі. Через п’ять секундsetTimeoutспрацьовує зворотний дзвінок. ParentComponent мутує джерело даних властивості ChildComponent, пов’язаної із введенням. ngOnInit НЕ ВАЖАЄ.

ngOnInitє одноразовим гачком. Її єдиною проблемою є ініціалізація.

ngDoCheck

ngDoCheckпожежі з кожним циклом виявлення змін. Виявлення змін у кутових прогонах часто. Виконання будь-якої дії призведе до його циклу. ngDoCheckпожежі за цих циклів. Використовуйте його з обережністю. Це може створити проблеми з продуктивністю при неправильній реалізації.

ngDoCheckдозволяє розробникам перевіряти свої дані вручну. Вони можуть умовно ініціювати нову дату подання заявки. Разом із цим ChangeDetectorRefрозробники можуть створювати власні чеки на виявлення змін.

import { Component, DoCheck, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'app-example', template: ` 

ngDoCheck Example

DATA: {{ data[data.length - 1] }}

` }) export class ExampleComponent implements DoCheck { lifecycleTicks: number = 0; oldTheData: string; data: string[] = ['initial']; constructor(private changeDetector: ChangeDetectorRef) { this.changeDetector.detach(); // lets the class perform its own change detection setTimeout(() => { this.oldTheData = 'final'; // intentional error this.data.push('intermediate'); }, 3000); setTimeout(() => { this.data.push('final'); this.changeDetector.markForCheck(); }, 6000); } ngDoCheck() { console.log(++this.lifecycleTicks); if (this.data[this.data.length - 1] !== this.oldTheData) { this.changeDetector.detectChanges(); } } }

Зверніть увагу на консоль і дисплей. Дані прогресують до "середнього" перед заморожуванням. За цей період відбувається три раунди виявлення змін, як зазначено на консолі. Ще один раунд виявлення змін відбувається, коли "остаточний" пересувається до кінця this.data. Потім відбувається останній раунд виявлення змін. Оцінка оператора if визначає, що оновлення подання не потрібні.

Summary: Class instantiates after two rounds of change detection. Class constructor initiates setTimeout twice. After three seconds, the first setTimeout triggers change detection. ngDoCheck marks the display for an update. Three seconds later, the second setTimeout triggers change detection. No view updates needed according to the evaluation of ngDoCheck.

Warning

Before proceeding, learn the difference between the content DOM and view DOM (DOM stands for Document Object Model).

The content DOM defines the innerHTML of directive elements. Conversely, the view DOM is a component’s template excluding any template HTML nested within a directive. For a better understanding, refer to this blog post.

ngAfterContentInit

ngAfterContentInit fires after the component’s content DOM initializes (loads for the first time). Waiting on @ContentChild(ren) queries is the hook’s primary use-case.

@ContentChild(ren) queries yield element references for the content DOM. As such, they are not available until after the content DOM loads. Hence why ngAfterContentInit and its counterpart ngAfterContentChecked are used.

import { Component, ContentChild, AfterContentInit, ElementRef, Renderer2 } from '@angular/core'; @Component({ selector: 'app-c', template: ` 

I am C.

Hello World!

` }) export class CComponent { } @Component({ selector: 'app-b', template: `

I am B.

` }) export class BComponent implements AfterContentInit { @ContentChild("BHeader", { read: ElementRef }) hRef: ElementRef; @ContentChild(CComponent, { read: ElementRef }) cRef: ElementRef; constructor(private renderer: Renderer2) { } ngAfterContentInit() { this.renderer.setStyle(this.hRef.nativeElement, 'background-color', 'yellow') this.renderer.setStyle(this.cRef.nativeElement.children.item(0), 'background-color', 'pink'); this.renderer.setStyle(this.cRef.nativeElement.children.item(1), 'background-color', 'red'); } } @Component({ selector: 'app-a', template: `

ngAfterContentInit Example

I am A.

BComponent Content DOM

` }) export class AComponent { }

The @ContentChild query results are available from ngAfterContentInit. Renderer2 updates the content DOM of BComponent containing a h3 tag and CComponent. This is a common example of content projection.

Summary: Rendering starts with AComponent. For it to finish, AComponent must render BComponent. BComponent projects content nested in its element through the element. CComponent is part of the projected content. The projected content finishes rendering. ngAfterContentInit fires. BComponent finishes rendering. AComponent finishes rendering. ngAfterContentInit will not fire again.

ngAfterContentChecked

ngAfterContentChecked fires after every cycle of change detection targeting the content DOM. This lets developers facilitate how the content DOM reacts to change detection. ngAfterContentChecked can fire frequently and cause performance issues if poorly implemented.

ngAfterContentChecked fires during a component’s initialization stages too. It comes right after ngAfterContentInit.

import { Component, ContentChild, AfterContentChecked, ElementRef, Renderer2 } from '@angular/core'; @Component({ selector: 'app-c', template: ` 

I am C.

Hello World!

` }) export class CComponent { } @Component({ selector: 'app-b', template: `

I am B.

CLICK ` }) export class BComponent implements AfterContentChecked { @ContentChild("BHeader", { read: ElementRef }) hRef: ElementRef; @ContentChild(CComponent, { read: ElementRef }) cRef: ElementRef; constructor(private renderer: Renderer2) { } randomRGB(): string { return `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)})`; } ngAfterContentChecked() { this.renderer.setStyle(this.hRef.nativeElement, 'background-color', this.randomRGB()); this.renderer.setStyle(this.cRef.nativeElement.children.item(0), 'background-color', this.randomRGB()); this.renderer.setStyle(this.cRef.nativeElement.children.item(1), 'background-color', this.randomRGB()); } } @Component({ selector: 'app-a', template: `

ngAfterContentChecked Example

I am A.

BComponent Content DOM

` }) export class AComponent { }

This hardly differs from ngAfterContentInit. A mere was added to BComponent. Clicking it causes a change detection loop. This activates the hook as indicated by the randomization of background-color.

Summary: Rendering starts with AComponent. For it to finish, AComponent must render BComponent. BComponent projects content nested in its element through the element. CComponent is part of the projected content. The projected content finishes rendering. ngAfterContentChecked fires. BComponent finishes rendering. AComponent finishes rendering. ngAfterContentChecked may fire again through change detection.

ngAfterViewInit

ngAfterViewInit fires once after the view DOM finishes initializing. The view always loads right after the content. ngAfterViewInit waits on @ViewChild(ren) queries to resolve. These elements are queried from within the same view of the component.

In the example below, BComponent’s h3 header is queried. ngAfterViewInit executes as soon as the query’s results are available.

import { Component, ViewChild, AfterViewInit, ElementRef, Renderer2 } from '@angular/core'; @Component({ selector: 'app-c', template: ` 

I am C.

Hello World!

` }) export class CComponent { } @Component({ selector: 'app-b', template: `

I am B.

` }) export class BComponent implements AfterViewInit { @ViewChild("BStatement", { read: ElementRef }) pStmt: ElementRef; constructor(private renderer: Renderer2) { } ngAfterViewInit() { this.renderer.setStyle(this.pStmt.nativeElement, 'background-color', 'yellow'); } } @Component({ selector: 'app-a', template: `

ngAfterViewInit Example

I am A.

BComponent Content DOM

` }) export class AComponent { }

Renderer2 changes the background color of BComponent’s header. This indicates the view element was successfully queried thanks to ngAfterViewInit.

Summary: Rendering starts with AComponent. For it to finish, AComponent must render BComponent. BComponent projects content nested in its element through the element. CComponent is part of the projected content. The projected content finishes rendering. BComponent finishes rendering. ngAfterViewInit fires. AComponent finishes rendering. ngAfterViewInit will not fire again.

ngAfterViewChecked

ngAfterViewChecked fires after any change detection cycle targeting the component’s view. The ngAfterViewChecked hook lets developers facilitate how change detection affects the view DOM.

import { Component, ViewChild, AfterViewChecked, ElementRef, Renderer2 } from '@angular/core'; @Component({ selector: 'app-c', template: ` 

I am C.

Hello World!

` }) export class CComponent { } @Component({ selector: 'app-b', template: `

I am B.

CLICK ` }) export class BComponent implements AfterViewChecked { @ViewChild("BStatement", { read: ElementRef }) pStmt: ElementRef; constructor(private renderer: Renderer2) { } randomRGB(): string { return `rgb(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)})`; } ngAfterViewChecked() { this.renderer.setStyle(this.pStmt.nativeElement, 'background-color', this.randomRGB()); } } @Component({ selector: 'app-a', template: `

ngAfterViewChecked Example

I am A.

BComponent Content DOM

` }) export class AComponent { }

Summary: Rendering starts with AComponent. For it to finish, AComponent must render BComponent. BComponent projects content nested in its element through the element. CComponent is part of the projected content. The projected content finishes rendering. BComponent finishes rendering. ngAfterViewChecked fires. AComponent finishes rendering. ngAfterViewChecked may fire again through change detection.

Clicking the element initiates a round of change detection. ngAfterContentChecked fires and randomizes the background-color of the queried elements each button click.

ngOnDestroy

ngOnDestroy fires upon a component’s removal from the view and subsequent DOM. This hook provides a chance to clean up any loose ends before a component’s deletion.

import { Directive, Component, OnDestroy } from '@angular/core'; @Directive({ selector: '[appDestroyListener]' }) export class DestroyListenerDirective implements OnDestroy { ngOnDestroy() { console.log("Goodbye World!"); } } @Component({ selector: 'app-example', template: ` 

ngOnDestroy Example

TOGGLE DESTROY

I can be destroyed!

` }) export class ExampleComponent { destroy: boolean = true; toggleDestroy() { this.destroy = !this.destroy; } }

Summary: The button is clicked. ExampleComponent’s destroy member toggles false. The structural directive *ngIf evaluates to false. ngOnDestroy fires. *ngIf removes its host . This process repeats any number of times clicking the button to toggle destroy to false.

Conclusion

Remember that certain conditions must be met for each hook. They will always execute in order of each other regardless. This makes hooks predictable enough to work with even if some do not execute.

With lifecycle hooks, timing the execution of a class is easy. They let developers track where change detection is occurring and how the application should react. They stall for code that requires load-based dependencies available only after sometime.

The component lifecycle characterizes modern front end frameworks. Angular lays out its lifecycle by providing the aforementioned hooks.

Sources

  • Angular Team. “Lifecycle Hooks”. Google. Accessed 2 June 2018
  • Gechev, Minko. “ViewChildren and ContentChildren in Angular”. Accessed 2 June 2018

Resources

  • Angular Documentation
  • Angular GitHub Repository
  • Lifecycle Hooks in Depth