Chapter 10. Change Detection

Change detection is one of the most important features in Angular 2. It covers everything from how is data-binding working to who is checking changes? By understanding these important topics, you can see the logic that is running internally in Angular2. And, you can also understand ways of using performance tuning. By controlling change detection, you can build a dramatically faster application.

What’s Change Detection?

At first, you should review more about rendering. Rendering is a process to map models to views. The models can be primitives, objects, arrays or any JavaScript data. And the views can be headers, paragraphs, forms, buttons or any user-interface elements. Generally, those are represented as the Document Object Model (DOM).

Well, a simple rendering example is as follows:

<h1 id="greeting"></h1>

<script>
document.getElementById("greeting").innerHTML = "Hello World!";
</script>

This is a very easy example, because this model will never change, so rendering is needed only once. And it gets so complex when the data changes at runtime. Rendering multiple times to sync models and views, we must think about the following things:

  • What change happened?
  • Where did the change happen at?
  • When did the change happen?
  • How will the view be updated?

The basic role of change detection is to manage these things above. When a change happens, a manager of the component states, change detector, detects the change and notifies it to the renderer to update the view. Simply, a change detector has two tasks:

  • Detect changes from models
  • Notify changes to views

Changes and Events

Well, we figured out about the roll of change detection. But then you may wonder; “what’s a change?”. A change is the difference between an old model and a new model. In other words, a change makes a new model. Let’s look at the following code:

@Component({
  template: `
  <span>{{counter}}</span>
  <button (click)="countUp()">Count up</button>
  `
})
class MyComponent {
  counter = 0;

  countUp() {
    this.counter++;
  }
}

Beginning rendering, {{counter}} in the template will be rendererd as 0 by view interpolation. And when the button is clicked, the counter property will be changed in the click event handler, and finally counted up so counter will be applied to the view; <span>1</span>. In this case, click event causes a change and the change is a mutation of the property.

Let’s take a look at the next example:

class MyComponent {
  counter = 0;

  ngOnInit() {
    setInterval(() => {
      this.counter++;
    }, 1000);
  }
}

This component has a timer to count up its counter every second. In this case, it’s timer event causes a change. Finally, let’s look at the following example:

class MyComponent {
  data = {};

  constructor(private http: Http) {}

  ngOnInit() {
    this.http.get('/data.json')
      .map(res => res.json())
      .subscribe(data => {
        this.data = data;
      });
  }
}

This component will send an HTTP request once it is initialized.And when that response will be back, the data property of the component will be updated. In this case, it’s a XHR callback that causes a change.

In summary, there are three things that can cause a change of the model:

  • Events: click, blur, submit, ...
  • Timers: setInterval, setTimeout, ...
  • XHRs: Ajax(GET, POST, ...)

And these have a thing in common: they are asynchronous operations. And now, we can say also that: all asynchronous operations can cause changes.

Great, you learned about what causes changes and when it’s caused! But you don’t know yet who notifies the changes to views. So next, we’ll dive into a mechanism that allows Angular to detect changes anytime. It’s called Zone.

Zones

Zones is one of proposals of the next ECMAScript specification. An Angular team is developing its implementation as zone.js. It says in that source code:

Zone is a mechanism for intercepting and keeping track of asynchronous work.

A Zone is a global object that is configured with rules about how to intercept and keep track of the asynchronous callbacks. Zone has these responsibilities:

  1. Intercept asynchronous task scheduling.
  2. Wrap callbacks for error-handling and zone tracking across async operations.
  3. Provide a way to attach data to zones.
  4. Provide a context specific last frame error handling.
  5. Intercept blocking methods.

As a simple example, consider the following code:

Zone.current.fork({
  onInvokeTask: (parent, current, target, task) => {
    console.log('Before task');
    parent.invokeTask(target, task);
    console.log('After task');
  }
}).run(() => {
  setTimeout(() => {
    console.log('Task');
  }, 1000);
});

The above will log:

'Before task'
'Task'
'After task'

This is not magic! Zones allow you to hook handlers on any async tasks. In Angular, there is NgZone, which is a customized zone for Angular. You can see that at the implementation of ApplicationRef:

// summarized code
class ApplicationRef {
  constructor(private zone: NgZone) {
    this.zone.onMicrotaskEmpty
      .subscribe(() => { 
        this.zone.run(() => { 
          this.tick();
         }); 
      });
  }

  tick() {
    this.changeDetectorRefs
      .forEach((ref) => ref.detectChanges());
  }
}

In the above, when the zone has no tasks, a handler of that event calls tick(). It executes change detection for each component of the application.

Summarizing the above:

  1. Async operations are scheduled as a task
  2. Zones observe the task execution
  3. Angular handles events caused by execution of async operations
  4. Change detectors run in all components

It’s probably difficult to understand all about zones, but its behaviors in the application is simple. You don’t have to consider about when and where a change happens. Angular is observing those always and detects all changes.

Change Detectors and Components

So far, we outlined the change detection of Angular 2. Here, we’ll talk about its performance and how data-binding works in your application.

As you know, an application is a tree of components. And now an important thing is that each component has its own change detector. It means that an application is a tree of change detectors.

By the way, you might wonder. Who does make change detectors? It’s good question. They’re made by code generation. Angular 2 generates them for each component. And those codes are never in polymorphic. There are monomorphic VM-friendly codes. This is one of why new Angular is dramatically fast. Basically, each component can execute hundreds of thousands of detections within a couple of milliseconds. You can make fast your application without performance tuning.

The another reason is caused by the change detector tree. In Angular 2, any data flows from top to bottom. Let’s see the following components:

@Component({
  selector: 'child', 
  template: '<p>{{text}}</p>'
})
class ChildComponent {
  @Input() text;
}

@Component({
  selector: 'parent', 
  template: '<child [text]="foo"></child>',
  directives: [ChildComponent]
})
class ParentComponent {
  foo = 'bar';
}

Change detection always begins at the root component. So in the above example, a detection of ParentComponent is earier than ChildComponent, and as a result, the foo property is passed to the child as text input. Then, ChildComponent detects a change of text property; it changed from "" into "bar". This is simple but very important. After ParentComponent was checked, its change detector doesn’t have to run, because it can be updated by only its parent. For single change detection, every component will be checked only once.

Change Detectors Tree

OnChanges

When any changes of input properties in your component are detected, an event is caused. Then you can get detail those changes as an argument. For example:

import {OnChanges} from '@angular/core';

@Component({...})
class MyComponent implements OnChanges {
  @Input() prop;

  ngOnChanges(changes: {[propName: string]: SimpleChange}) {
    let newValue = changes['prop'].currentValue;
  }
}

The OnChanges interface defines a method, ngOnChanges. The argument, changes, has all of the changes of the component as a key-value map. A value of the map is SimpleChange, which has two properties: previousValue and currentValue. The earlier is an old value before the change, and the later is a new value.

A point you should be careful with is that the hook won’t be called by itself. An example is the following:

@Component({...})
class MyComponent {
  @Input() prop = null;

  ngOnChanges(changes) {
    // never called after onSomeEvent
  }

  onSomeEvent() {
    this.prop = someExpression();
  }
}

prop is a property of MyComponent and it is modified by itself. In a case like this, ngOnChanges is never called. It’s called only when its property is changed by its parent. So, you must never modify any input properties in your own component. You should access those as read-only.

Change Detection Tuning

Now, you’ve finished learning about the basis of change detection! And later, we’ll talk about how to tune your change detection. In Angular 2, you can configure change detection behavior for your application and optimize it. As mentioned earlier, change detection is very fast without any tuning. But if you want, you may make its change detection smarter and quite faster.

Change Detection Strategy

In order to customize change detection of your component, there is a utility: ChangeDetectionStrategy. Using this, you can change the strategy of change detection for each component.

First, let’s imagine a component like this:

@Component({
  selector: 'profile-card',
  template: `
  <div>
    <profile-name [name]="profile.name"></profile-name>
    <profile-age [age]="profile.age"></profile-age>
  </div>
  `,
  directives: [ProfileNameComponent, ProfileAgeComponent]
})
class ProfileCardComponent {
  @Input() profile;
}

ProfileCardComponent is a component, which has profile field as an input property. And its template (in other words, view) depends on only that property. So, this component and its child components depend on an input property.

Without a strategy, change detection runs from the root component to every leaf component. But in this case, it’s unnecessary to check children of ProfileCardComponent if profile is not changed. Simply, you can stop checking at ProfileCardComponent. Well, there is a strategy for the case that uses OnPush.

OnPush Strategy

OnPush* is an option of change detection strategy. Let’s look at the following code:

@Component({
  selector: 'profile-card',
  template: `
  <div>
    <profile-name [name]="profile.name"></profile-name>
    <profile-age [age]="profile.age"></profile-age>
  </div>
  `,
  directives: [ProfileNameComponent, ProfileAgeComponent],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ProfileCardComponent {
  @Input() profile;
}

There is a new component setting, changeDetection, which take a strategy provided as a static field of ChangeDetectionStrategy. Using this strategy, the component skips its children’s change detection when no changes are caused by its parent. Reducing the number of checks affects a performance of the application directly. As far as possible, you should use the OnPush strategy on your components, and for this, you should increase components that depend on only input properties.

OnPush Strategy

Mutable and Immutable

Using the OnPush strategy, it’s very important to learn about mutable and immutable. Basically, change detectors can detect only reference changes. Let’s see the next bad example:

@Component({
  template: `
  <div>
    <profile-card [profile]="profile"></profile-card>
  </div>
  `,
  directives: [ProfileCardComponent],
})
class AppComponent {
  profile;

  ngOnInit() {
    this.profile = { name: 'Brad Green' };
  }

  changeProfile() {
    this.profile.name = 'Igor Minar'
  }
}

In this case, the profile property of the AppComponent is modified but its reference is never changed. With mutable data like this, the OnPush strategy doesn’t work well because the change detector of AppComponent cannot know whether profile was changed. So, the OnPush strategy needs immutable data to detect changes at the component closer to the root.

This is the rewritten code:

@Component({
  template: `
  <div>
    <profile-card [profile]="profile"></profile-card>
  </div>
  `,
  directives: [ProfileCardComponent],
})
class AppComponent {
  profile;

  ngOnInit() {
    this.profile = new Profile({ name: 'Brad Green' });
  }

  changeProfile() {
    let newProfile = profile.setName('Igor Minar'); // make new reference
    profile === newProfile; // false
    this.profile = newProfile;
  }
}

The reference of profile is changed in changeProfile(). Because profile became immutable, ProfileCardComponent can check whether profile was changed strictly, and it can control its children’s change detection.

Using Observable

The OnPush strategy is an easy and great way to improve the performance of your application. Well, there is another way to get better performace. It is to use observable properties with ChangeDetectorRef utility. This way allows you to control all of the change detection manually.

ChangeDetectorRef

ChangeDetectorRef is a reference of the compoment’s change detector. This can be implemented by dependency injection at the component:

import {ChangeDetectorRef} from '@angular/core';

@Component({})
class MyComponent {
  constructor(private cdRef: ChangeDetectorRef) {}
}

ChangeDetectorRef has some methods. In those, markForCheck() is the most useful method. It marks components from the root to the place as to be checked. Let’s take a look at the following:

@Component({
  selector: 'child',
  template: `<p>{{counter}}</p>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ChildComponent {
  counter = 0;
  constructor(private cdRef: ChangeDetectorRef) {}

  ngOnInit() {
    setInterval(() => {
      this.counter++;
      this.cdRef.markForCheck();
    }, 1000);
  }
}

@Component({
  selector: 'parent',
  template: `<child></child>`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
class ParentComponent {
}

ChildComponent is defined as the OnPush component, but it doesn’t have any input properties. When will ChildComponent be checked? Well, look at its ngOnInit! In the interval function, cdRef.markForCheck() is called per 1 second. Even without any changes or events, if markForCheck() was called, that component is included in the next change detection process.

markForCheck

Next, we talk about cdRef.detach() and cdRef.reattach(). They allow you to turn on/off change detection at the component. Let’s see the following example:

@Component({
  template: `
    Detach: <input type="checkbox" (change)="detachCD($event.target.checked)">
    <p>{{counter}}</p>
  `,
})
class ChildComponent {
  counter = 0;
  constructor(private cdRef: ChangeDetectorRef) {}

  ngOnInit() {
    setInterval(() => {
      this.counter++;
    }, 1000);
  }

  detachCD(checked) {
    if (checked) {
      this.cdRef.detach();
    } else {
      this.cdRef.reattach();
    }
  }
}

This component has a checkbox to toggle its own change detection. When the checkbox is checked, cdRef.detach() will be called, and after that, the component and its children will be never checked. As a result, the view of the sub-tree from the component will be frozen.

And when you uncheck it, cdRef.reattach() will be called. After that, the component will join the change detector tree again.

Summary

Change detection of Angular 2 is a great system to manage your application states, and it can provide you with an easy way to improve the performance.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset