By Mihail Poddubsky.

Angular 2 has many new features and this article discusses the one I find most important—the change detection system, which largely propelled Angular 1 to its popularity. In Angular 1, change detection was performed with the help of the following methods: $scope.$watch() and $scope.$digest(). In Angular 2, the familiar $scope methods are not used.

So what's the alternative in Angular 2? To begin answering this question, let's look at what can cause UI changes in an application. Usually, it's one of three scenarios:

  • Events caused by the user (clicks, submits, mousemoves, and so forth)
  • XMLHttpRequests, WebSockets
  • Timers: setTimeout() and setInterval()

For such cases, Angular 1 offered the use of internal methods for DOM and model synchronization, such as ng-click, $http.get(), $timer, and so on. All of these methods have something in common: inside the data, the $scope.$digest() method is called.

Now, let's look at a component sample written in Angular 2:

import { Component, NgZone } from '@angular/core';
import {Component, ElementRef, OnInit} from '@angular/core';

@Component({
   selector: 'ng-zone-demo',
      template: `
         <div>Angular 2 is not so.. {{phrase}}</div>
         <button class="change-button"
            (click)="changePhrase()">Click me</button>`
})

export class NgZoneDemoComponent{
   phrase:string = 'difficult';

   changePhrase () {
      this. phrase = 'simple';
   }

As you can see, there are no familiar ng-click or $digest, but when you call click, DOM is updated. If you switch (click)="changePhrase" for the familiar addEventListenter('click', this.changePhrase), the situation is not changed. The synchronization between the feature phrase and UI still takes place.

This happens because in Angular 2, Zone.js library is used to perform change detection. Developers define Zone as an execution context. Zone also provides the fork method, allowing you to create a new child zone and expand its basic behavior. Angular 2, in turn, uses NgZone—a branch of the standard Zone.

In addition, as soon as we connect Zone.js, the library redefines global asynchronous methods, so that, in fact, you are working with already modified methods. The following is an incomplete list of methods redefined by Zone:

  • Zone.setInterval()
  • Zone.setTimeout ()
  • Zone.alert()
  • Zone.MutationObserver ()
  • Zone.requestAnimationFrame()
  • Zone.addEventListener()
  • Zone.removeEventListener()

Every time after handlers are called in these methods, NgZone calls the onTurnDone event, notifying Angular 2 that it is necessary to perform change detection. But, you have to understand that change detection affects performance. This is especially noticeable when you call such events as mousemove, or the requestAnimationFrame() method.

Fortunately, NgZone provides the runOutsideAngular (fn) method, which performs the function, but does not cause onTurnDone events. Therefore, change detection does not occur.

Let's try to change the handler and see the result:

export class NgZoneDemoComponent {
   phrase:string = 'difficult';

constructor(private ngZone: NgZone) {}

   changePhrase () {
      ngZone.runOutsideAngular (()=> {
         this. phrase = 'tricky';
         });
      }
   }
}

After calling the events handler, change detection does take place.

This happens because, when adding the event handler (click)="changePhrase", you activate the modified Zone.addEventListener(), and not the standard window.addEventListener(). And, calling the event click launches change detection anyway, having already performed the event handler changePhrase().

So, what do you really need to do for change detection not to be executed? Let's look at the modified component one more time:

import { Component, NgZone } from '@angular/core';
import {Component, ElementRef, OnInit} from
   '@angular/core';

@Component({
   selector: 'ng-zone-demo',
   template: `
      <div>Angular 2 is not so.. {{phrase}}</div>
      <button class="change-button">Click me</button>
   `
})

export class NgZoneDemoComponent {
   phrase:string = 'difficult';
   buttonElement: any = null;

      constructor(private ngZone:
         NgZone, private element: ElementRef) {}

      ngOnInit() {
         this.buttonElement = this.element.nativeElement.
            querySelector('.change-button');

         this.ngZone.runOutsideAngular(()=> {

            this.buttonElement.addEventListener('click',
               this.changePhrase);
         });
      }
      changePhrase () {
         this.phrase = 'this change will not be checked';
   }
}

This time, we have added the event handler within the runOutsideAngular () method, so in this case NgZone does not run change detection. However, a reverse situation is possible, when we use handlers of a third-party API, or want to start change detection at a specific moment. For this, NgZone provides the run () method.

import { Component, NgZone } from '@angular/core';

@Component({
   selector: 'ng-zone-demo',
   template: `
      <div>Changes will be triggered when counter is 100</div>
      <div>counter is {{ counter }}</div>
      <button (click)="runCounter()">Click me</button>
})
export class NgZoneDemoComponent {

   counter:number = 0;

   constructor(private ngZone: NgZone) {

   runCounter () {
      this.ngZone.runOutsideAngular(()=> {
      var intervalId = window.setInterval(()=> {
         this.counter += 1;
            if(this.counter === 100) {
               window.clearInterval(intervalId);
                  this.ngZone.run(() => {
                     window.console.log('Executes the function
                        synchronously within the Angular zone');
                  });
            }
         });
      });
   }
}

In this example, DOM is updated only when the counter feature has already reached the desired value, immediately after calling ngZone.run (). Using the ngZone.runOutsideAngular () method allows you to prevent a change detection launch every time you call setInterval ().

Now that you understand what you need ngZone for, let's look at the process of launching change detection in more detail. What exactly happens when you run the onTurnDone event?

The Angular 2 ApplicationRef subscribes to the event onTurnDone to launch the tick () method and start the process of verifying changes in all components.

class ApplicationRef {
   changeDetectorRefs:ChangeDetectorRef[] = [];
   constructor(private zone: NgZone) {
      this.zone.onTurnDone
         .subscribe(() => this.zone.run(() => this.tick());
   }
   tick() {
      this.changeDetectorRefs
         .forEach((ref) => ref.detectChanges());
   }
}

Each Angular 2 component has its own copy of ChangeDetectorRef, which is responsible for the monitoring and verification of changes. You know the application of Angular 2 is a tree of components, which makes it possible to perform change detection unidirectionally. This means that the checking of each component is performed from top to bottom, beginning with the root component. This helps you achieve more predictable behavior when compared to Angular 1, where the change detection process occurs in a cycle. In addition, because of the predictable component structure, it is now possible to optimize the change detection logic, which has a positive impact on performance.

UI

Figure 1: Traversing changes in Angular 1 and Angular 2

By default, Angular 2 checks each tree component on launching change detection. It's a process largely similar to that in Angular 1, and in the majority of cases does not cause any performance problems.

But, let's look at the following example:

@Component({
   selector: 'change-detection-demo',
   template: `
      <row *ngFor="let rowValue of rows"
         [data]="rowValue"></row>
   `,
   directives: [RowComponent]
})
class App {
   private rows: Array<number> = [];

   constructor() {
      for (let i = 0; i < 100; i++) {
         this.rows.push(i);
      }

      window.setTimeout(() => {
         this.rows[0] = 999;
      }, 1000)
   }
}
}

In this case, it's a component comprised of a list of other components:

import {Component, Input} from '@angular/core';

@Component({
   selector: 'row',
   template: `
      <li>{{ rowValue }}</li>
   `
})
export class RowComponent {
   @Input() public value: number;

   get rowData(): number {
      console.log('getting row value);
      return this.data;
   }

   ngOnChanges(): void {
      console.log('change');
   }
}

Angular 2 completely controls the processes occurring in the component, as well as providing you with Lifecycle hooks for different events. One of them, ngOnChanges, is called when updating the component input values.

In the example, an array of values is created. After a while, the value of one of the components changes. We expect the ngOnChanges method will be called once, but in fact it will be called for all 100 components in this case. Why is this happening? Angular 2 does not know exactly which side effects a change in one component can cause. Therefore, as already mentioned, Angular 2 checks each tree component as soon as it is notified of a change.

However, because it is known that the row components do not affect each other, the default behavior of change detection can be altered. For this, Angular 2 provides ChangeDetectionStrategy, which has two types of behavior: Default and OnPush.

Let's add OnPush to the component:

import {ChangeDetectionStrategy, Component, Input}
   from '@angular/core';

@Component({
   selector: 'row',
   template: `
      <li>{{ rowValue }}</li>
   `,
   changeDetection: ChangeDetectionStrategy.OnPush
})
export class RowComponent {
   @Input() public value: number;

   get rowValue(): number {
      console.log('getting row value);
      return this.data;
   }

   ngOnChanges(): void {
      console.log('change');
   }
}

Now, the component will be updated only if the reference of the input value has changed or an event has been called in the component itself.

In the preceding case, ngOnChanges will be called only once.

To conclude, it can be said that the change detection mechanism in Angular 2 is quite similar to Angular 1. But at the same time, it has a number of important changes. Change detection is not cyclic, resulting in better performance. Besides, developers can benefit from an important feature of being able to configure this mechanism to achieve even better performance.

About the Author

Mihail Poddubsky is a front-end developer at Itransition, an international software development and systems integration company. He is a highly motivated Web developer with a strong theoretical base and production experience in Web development with a focus on project improvements and system stabilization. Mihail effectively combines technical and analytical skills, and enjoys working on a team during all stages of the enterprise project lifecycle. His technical interests are in JavaScript, TypeScript, HTML5, CSS3, CSS-preprocessors such as LESS, and JavaScript frameworks, including AngularJS. He graduated from Belarusian State University of Informatics and Radioelectronics with a degree in systems engineering.


Tags: UI, user interface, DOM, Angular 2, change detection system, change detection, ngZone