Wednesday, May 2, 2018

Change Detection in Angular: Everything You Need to Know

This article on change detection in Angular was originally published on the Angular In Depth blog, and is republished here with permission.

If you're like me and looking to gain a comprehensive understanding of the change detection mechanism in Angular, you basically have to explore the sources, since there’s not much information available on the web.

Most articles mention that each component has its own change detector which is responsible for checking the component, but they don’t go beyond that and mostly focus on use cases for immutables and change detection strategy.

This article provides you with the information required to understand why use cases with immutables work and how change detection strategy affects the check. Also, what you’ll learn from this article will enable you to come up with various scenarios for performance optimization on your own.

The first part of this article is pretty technical and contains a lot of links to the sources. It explains in detail how the change detection mechanism works under the hood. Its content is based on the newest Angular version (4.0.1 at time of writing). The way the change detection mechanism is implemented under the hood in this version is different from the earlier 2.4.1. If interested, you can read a little about how it worked in this Stack Overflow answer.

The second half of the article shows how change detection can be used in the application, and its content is applicable for both earlier 2.4.1 and the newest 4.0.1 versions of Angular, since the public API hasn’t changed.

View as a Core Concept

An Angular application is a tree of components. However, under the hood, Angular uses a low-level abstraction called view. There’s a direct relationship between a view and a component: one view is associated with one component and vice versa. A view holds a reference to the associated component class instance in the component property. All operations — like property checks and DOM updates — are performed on views. Hence, it’s more technically correct to state that Angular is a tree of views, while a component can be described as a higher-level concept of a view. Here’s what you can read about the view in the sources:

A View is a fundamental building block of the application UI. It is the smallest grouping of Elements which are created and destroyed together.

Properties of elements in a View can change, but the structure (number and order) of elements in a View cannot. Changing the structure of Elements can only be done by inserting, moving or removing nested Views via a ViewContainerRef. Each View can contain many View Containers.

In this article, I’ll be using notions of component view and component interchangeably.

It’s important to note here that all articles on the web and answers on Stack Overflow regarding change detection refer to the View I’m describing here as Change Detector Object or ChangeDetectorRef. In reality, there’s no separate object for change detection and View is what change detection runs on.

Each view has a link to its child views through the nodes property, and hence can perform actions on child views.

View State

Each view has a state, which plays a very important role because, based on its value, Angular decides whether to run change detection for the view and all its children, or skip it. There are many possible states, but the following ones are relevant in the context of this article:

  1. FirstCheck
  2. ChecksEnabled
  3. Errored
  4. Destroyed

Change detection is skipped for the view and its child views if ChecksEnabled is false or view is in the Errored or Destroyed state. By default, all views are initialized with ChecksEnabled unless ChangeDetectionStrategy.OnPush is used. More on that later. The states can be combined: for example, a view can have both the FirstCheck and ChecksEnabled flags set.

Angular has a bunch of high-level concepts to manipulate the views. I’ve written about some of them here. One such concept is ViewRef. It encapsulates the underlying component view and has an aptly named method detectChanges. When an asynchronous event takes place, Angular triggers change detection on its top-most ViewRef, which after running change detection for itself runs change detection for its child views.

This viewRef is what you can inject into a component constructor using the ChangeDetectorRef token:

export class AppComponent {  
    constructor(cd: ChangeDetectorRef) { ... }

This can be seen from the class's definition:

export declare abstract class ChangeDetectorRef {  
    abstract checkNoChanges(): void;  
    abstract detach(): void;  
    abstract detectChanges(): void;  
    abstract markForCheck(): void;  
    abstract reattach(): void;  
}

export abstract class ViewRef extends ChangeDetectorRef {  
   ...
}

Change Detection Operations

The main logic responsible for running change detection for a view resides in the checkAndUpdateView function. Most of its functionality performs operations on child component views. This function is called recursively for each component, starting from the host component. It means that a child component becomes a parent component on the next call as a recursive tree unfolds.

When this function is triggered for a particular view, it does the following operations in the specified order:

  1. sets ViewState.firstCheck to true if a view is checked for the first time and to false if it was already checked before
  2. checks and updates input properties on a child component/directive instance
  3. updates child view change detection state (part of change detection strategy implementation)
  4. runs change detection for the embedded views (repeats the steps in the list)
  5. calls OnChanges lifecycle hook on a child component if bindings changed
  6. calls OnInit and ngDoCheck on a child component (OnInit is called only during first check)
  7. updates ContentChildren query list on a child view component instance
  8. calls AfterContentInit and AfterContentChecked lifecycle hooks on child component instance (AfterContentInit is called only during first check)
  9. updates DOM interpolations for the current view if properties on current view component instance changed
  10. runs change detection for a child view (repeats the steps in this list)
  11. updates ViewChildren query list on the current view component instance
  12. calls AfterViewInit and AfterViewChecked lifecycle hooks on child component instance (AfterViewInit is called only during first check)
  13. disables checks for the current view (part of change detection strategy implementation)

There are few things to highlight based on the operations listed above.

The first thing is that the onChanges lifecycle hook is triggered on a child component before the child view is checked, and it will be triggered even if changed detection for the child view will be skipped. This is important information, and we’ll see how we can leverage this knowledge in the second part of the article.

The second thing is that the DOM for a view is updated as part of a change detection mechanism while the view is being checked. This means that if a component is not checked, the DOM is not updated even if component properties used in a template change. The templates are rendered before the first check. What I refer to as DOM update is actually interpolation update. So if you have <span>some </span>, the DOM element span will be rendered before the first check. During the check only the part will be rendered.

Another interesting observation is that the state of a child component view can be changed during change detection. I mentioned earlier that all component views are initialized with ChecksEnabled by default, but for all components that use the OnPush strategy, change detection is disabled after the first check (operation 9 in the list):

if (view.def.flags & ViewFlags._OnPush_) {  
  view.state &= ~ViewState._ChecksEnabled_;
}

It means that during the following change detection run the check will be skipped for this component view and all its children. The documentation about the OnPush strategy states that a component will be checked only if its bindings have changed. So to do that, the checks have to be enabled by setting the ChecksEnabled bit. And this is what the following code does (operation 2):

if (compView.def.flags & ViewFlags._OnPush_) {  
  compView.state |= ViewState._ChecksEnabled_;
}

The state is updated only if the parent view bindings changed and child component view was initialized with ChangeDetectionStrategy.OnPush.

Finally, change detection for the current view is responsible for starting change detection for child views (operation 8). This is the place where the state of the child component view is checked and if it’s ChecksEnabled, then for this view the change detection is performed. Here is the relevant code:

viewState = view.state;  
...
case ViewAction._CheckAndUpdate_:  
  if ((viewState & ViewState._ChecksEnabled_) &&  
    (viewState & (ViewState._Errored_ | ViewState._Destroyed_)) === 0) {  
    checkAndUpdateView(view);
  }  
}

Now you know that the view state controls whether change detection is performed for this view and its children or not. So the question begs: can we control that state? It turns out we can, and this is what the second part of this article is about.

Some lifecycle hooks are called before the DOM update (3,4,5) and some after (9). So if you have the components hierarchy A -> B -> C, here is the order of hooks calls and bindings updates:

A: AfterContentInit  
A: AfterContentChecked  
A: Update bindings  
    B: AfterContentInit  
    B: AfterContentChecked  
    B: Update bindings  
        C: AfterContentInit  
        C: AfterContentChecked  
        C: Update bindings  
        C: AfterViewInit  
        C: AfterViewChecked  
    B: AfterViewInit  
    B: AfterViewChecked  
A: AfterViewInit  
A: AfterViewChecked

Continue reading %Change Detection in Angular: Everything You Need to Know%


by Maximus Koretskyi via SitePoint

No comments:

Post a Comment