Change detection
in Angular
Andrei Antal
Frontend engineer, Qualitance
ngBucharest meetup, 27 July 2017
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','Void Canvas');
}
}
});
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
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);
}
}
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
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);
}
}
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 ;)
See you next time!
Thanks for coming !
ngBucharest
Change detection in Angular
By Andrei Antal
Change detection in Angular
- 1,601