Change detection

in Angular

Andrei Antal

Frontend engineer, Qualitance

Cluj JavaScripters meetup, 23 November 2017

Servus! A bit about me

Andrei Antal

frontend engineer @ Qualitance

 

  • JavaScript technologies enthusiast
  • Community activist
  • Occasional speaker
  • member of ngCommunity

Contact me at:

antal.a.andrei@gmail.com

@andrei_antal

ngBucharest

JSHacks Bucharest

War...

...war never changes

State in Web Apps

User Interface = function(State)

const bookTitle = "Alchemy for beginners";

const chaptersTitles = [ "Equipment" ]

let book = {
    title: bookTitle,
    chapters: chapterTitles
  }

.....
      

Strings, numbers, arrays, objects etc.

Document Object Model

(DOM)

(rendering)

(Data projection)

State changeing over time

  • User input (event callbacks)
  • Async requests 
  • Timers (setInterval, setTimeout)

Server side rendering

Server

Client

request

website resources (html, js, css)

EVENT

request (state change request)

new website resources (html, js, css)

APP STATE

request (state change request)

new website resources (html, js, css)

EVENT

(re-render page)

(re-render page)

Change detection in MVC frameworks

CONTROLLER

MODEL

VIEW

Update

Notify

Update

User action

Backbone

app.Todo = Backbone.Model.extend({

    defaults: {
        title: '',
        completed: false
    },
    toggle: function () {
	this.save({
	    completed: !this.get('completed')
        });
    }
});
app.TodoView = Backbone.View.extend({
  template: `<div class="view">
      <input class="toggle" type="checkbox" 
        <%= completed?'checked':'' %>>
      <label><%- title %></label>
      <button class="destroy"></button>
    </div>
    <input class="edit" value="<%- title %>`,

  events: {
    'click .toggle': 'toggleCompleted',
  }

  initialize: function () {
    this.listenTo(this.model, 'change', this.render);
  },

  render: function () {
    this.$el.html(this.template(this.model.toJSON()));
    return this;
  },

  toggleCompleted: function () {
    this.model.toggle();
  },
})
  • Event listeners in view
  • Framework notifies view of model changes through model setters 
  • Similar to ExtJS and Dojo

Manual re-render

<div>
    Name: {{input type="text" value=name placeholder="Enter your name"}}
</div>
<div class="text">
    <p>My name is, <strong>{{name}}</strong> and I want to learn Ember!</p>
</div>
<button {{action changeName}}>Change Name to Void Canvas</button>

template

Text

App = Ember.Application.create();


App.ApplicationController = Ember.Controller.extend({
  actions:{
    changeName:function(){
      this.set('name','Andrei Antal');
    }
  }
});

js

Ember

Data binding

Ember

Data binding

AngularJS (1)

Digest cycle (dirty checking)

<div ng-controller="app">
    <input ng-model="a">
    <div>{{b}}</div>
</div>
app.controller('App', function() {
    $scope.a = 1;
    $scope.b = 2;
    $scope.c = 3;
});

Scope watch list

watch 1: a = 1

watch 2: b = 2

AngularJS (1)

Digest cycle (dirty checking)

Watch:  a = 8

Watch:  b = 3

Watch:  c = 5

Watch:  d = 1

scope watch list

digest cycle

Angular context

AngularJS (1)

Digest cycle (dirty checking)

  • Each scope has his own watch list

AngularJS (1)

Digest cycle (dirty checking)

Watch:  a = 8

Watch:  b = 3

Watch:  c = 5

Watch:  d = 1

watch list

digest cycle 1

Watch listener

Watch listener

digest cycle 2

  • Runs at least twice
  • Runs at most 10 times (throws error)

...

AngularJS (1)

Digest cycle (dirty checking)

React

React

VueJS

var vm = new Vue({
  data: {
    // declare message with an empty value
    message: ''
  },
  template: '<div>{{ message }}</div>'
})
// set `message` later
vm.message = 'Hello!'

Change detection in Angular (2...4...5...)

Triggering changes

@Component({
    selector: 'todo-item',
    template: `
      <div class="todo noselect">
        {{todo.owner.firstname}} - {{todo.description}} <br />
        completed: {{todo.completed}}
      </div>
      <button (click)="onToggle()">COMPLETE</button>`
})
export class TodoItem {
    @Input()
    todo:Todo;

    @Output()
    toggle = new EventEmitter<Object>();

    onToggle() {
        this.toggle.emit(this.todo);
    }
}
@Component()
class ContactsApp implements OnInit{

  contacts:Contact[] = [];

  constructor(private http: Http) {}

  ngOnInit() {
    this.http.get('/contacts')
      .map(res => res.json())
      .subscribe(contacts => this.contacts = contacts);
    setTimeout(() => {/*some changes*/})
  }
}

The event loop

console.log('1')
console.log('2')
console.log('3')
RESULTS:

1
2
3
console.log('1')
setTimeout(() => {
  console.log('2')
}, 0)
console.log('3')
STACK

console.log
setTimeout
console.log
RESULTS:

1
3
2

Synchronous

Asynchronous

Zone.js

function main() {
  console.log('1')
  setTimeout(() => {
    console.log('2')
  }, 0)
  console.log('3')
}

var myZoneSpec = {
  beforeTask: function () {
    console.log('Before task');
  },
  afterTask: function () {
    console.log('After task');
  }
};

var myZone = zone.fork(myZoneSpec);
myZone.run(main);

// Logs:
// Before task
// 1
// 3
// After task
// Before task
// 2
// After task

Zone.js

  • An execution context for asynchronous operations
  • Zones are able to track the context of asynchronous actions by monkey-patching them
function addEventListener(eventName, callback) {
     // call the real addEventListener
     callRealAddEventListener(eventName, function() {
        // first call the original callback
        callback(...);     
        // and then run Angular-specific functionality
        var changed = angular2.runChangeDetection();
        if (changed) {
            angular2.reRenderUIPart();
        }
     });
}

Zone.js and Angular

  • Angular comes with its own zone called NgZone
  • ApplicationRef listens to NgZones onTurnDone event
  • When it is fired it executes a tick() function which performs change detection.
// very simplified version of actual source
class ApplicationRef {

  changeDetectorRefs:ChangeDetectorRef[] = [];

  constructor(private zone: NgZone) {
    this.zone.onTurnDone
      .subscribe(() => this.zone.run(() => this.tick());
  }

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

Component tree and change detection

event

@Component({
    selector: 'todo-item',
    template: `
      <div class="todo noselect">
        {{todo.owner.firstname}} - {{todo.description}}
        completed: {{todo.completed}}
      </div>
      <button (click)="onToggle()">COMPLETE</button>`
})
export class TodoItem {
    @Input()
    todo:Todo;

    @Output()
    toggle = new EventEmitter<Object>();

    onToggle() {
        this.toggle.emit(this.todo);
    }
}

Change detection performance

export class Todo {
    constructor(public id: number,
        public description: string,
        public completed: boolean,
        public owner: Owner) {
    }
}

export class Owner {
  constructor(
      public firstName: string,
      public lastName: string) {
  }
}
// AppComponent

@Component({...})
export class AppComponent {
  todo: Todo = {...};

  toggle() {
    this.todo.completed = !this.todo.completed;
  }
}

Change detection performance

View as a core concept

  • 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.

View state

  • FirstCheck
  • ChecksEnabled
  • Errored
  • Destroyed

Angular triggers change detection on its top-most ViewRef, which after running change detection for itself runs change detection for its child views.

Change detection performance

  • By default, Angular Change Detection works by checking if the value of template expressions have changed. This is done for all components
  • By default, Angular does not do deep object comparison to detect changes, it only takes into account properties used by the template
  • It can perform hundreds of thousands of checks within a couple of milliseconds​
  • Angular generates VM friendly code.
  • Angular creates change detector classes at runtime for each component - monomorphic models

Optimise change detection

Reducing the number of checks - OnPush change detection

@Component({..})
export class App {
	data = { counter: 0 };
}

<my-counter [data]="data"></my-counter>

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

@Component({
  ...
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  @Input() data;
}
// won't trigger change detection
this.data.counter++;

// will trigger change detection
this.data = { counter: this.data.counter + 1 };

Optimise change detection

OnPush change detection - excluding parts of the component tree from running change detection

Optimise change detection

OnPush is more than just reference comparrison:

  • When using OnPush detectors, then the framework will check an OnPush component when any of its input properties changes, when it fires an event, or when an Observable fires an event

Optimise change detection

Using Observables as Inputs with OnPush change detection


@Component({
  ...
  template: `
    ...
    <my-counter [data]="data$"></my-counter>
  `
})
export class App {
  data$ = new BehaviorSubject({ counter: 0 });
  ...
}

  this.data$.next({ counter: ++this._counter });

@Component({
  ...
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  @Input() data: Observable<any>;
  ...

  ngOnInit() {
    this.data.subscribe((value) => {
      // local variable bound to our template
      this._data = value;
    });
  }
}

Optimise change detection

event

Optimise change detection


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

export class CounterComponent {
	constructor(private cd: ChangeDetectorRef) {}
  ...
  ngOnInit() {
    this.data.subscribe((value) => {
      this._data = value;
      // tell CD to verify this subtree
      this.cd.markForCheck();
    });
  }
}

Optimise change detection

Turning change detection off and triggering change detection manually


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

export class CounterComponent {
    
  constructor(private cd: ChangeDetectorRef) {}
  
  ...
  
  ngOnInit() {
    cd.detach();
    
    // get some stream of data    

    setInterval(() => {
      this.cd.detectChanges();
    }, 5000);
  }
}

Optimise change detection

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 {
   ...
}
export class AppComponent {
    constructor(cd: ChangeDetectorRef) { ... }

Summary

  • Change detection is the mechanism of keeping your JS data models in sync with the UI (DOM); is the most important and difficult task of a JavaScript framework

 

  • Angular change detection relies on each component having its own change detector; due to code generation optimisation the CD mechanism is super fast

 

  • CD can be further improved by leveraging immutable data structures and OnPush strategy

 

  • We can also control the CD mechanism turning then on and off

Resources and inspiration

Thanks ;)

ngBucharest

Cluj JavaScripters - Change detection in Angular

By Andrei Antal

Cluj JavaScripters - Change detection in Angular

  • 1,430