Change detection in simplified
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
Change detection in depth
onPush it to the limit
ANGULAR IS NOT SLOW ANYMORE
@FilipMam
how to understand what's going under the Angular's hood without losing your hair
I really need more followers than Kacper
Change detection revisited
Change detection simplified
@FilipMam
Agenda
-
What's up with CD (in Angular)?
-
How does it work by default?
-
How can we improve it?
@FilipMam
@FilipMam
DOM
Model-View
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`})
class UserDetail {
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
}
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`})
class UserDetail {
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
addMoney() {
this.user.money += 10;
}
}
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
<button (click)="addMoney()">Add money</button>
`})
class UserDetail {
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
addMoney() {
this.user.money += 10;
}
}
@FilipMam
DOM
Model
@FilipMam
DOM
Model
@FilipMam
DOM
Model
How does CD work in Angular?
@FilipMam
Changing model
-
Dom events
-
XHR requests
-
Timers
@FilipMam
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
<button (click)="addMoney()">Add money</button>
`})
class UserDetail {
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
addMoney() {
this.user.money += 10;
}
}
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
<button (click)="addMoney()">Add money</button>
`})
class UserDetail {
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
addMoney() {
this.user.money += 10;
}
}
@FilipMam
Angular's app structure
@FilipMam
Angular's app structure
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`})
class UserDetail {
@Input()
user: User;
}
User component
@FilipMam
@Component({
template: `
<user [user]="userFromParent"></user>
<button (click)="addMoney()">Add money</button>
`})
class ParentComponent {
userFromParent: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
addMoney() {
this.userFromParent.money += 10;
}
}
Parent component
@FilipMam
@FilipMam
App structure
Changing model
-
Dom events
-
XHR requests
-
Timers
@FilipMam
-
Input changes
@FilipMam
Zone.js
@FilipMam
Zone.js + Angular
class AppMain {
constructor(private zone: NgZone) {
this.zone.asyncEventDone
.subscribe(() => runAngularChangeDetection());
}
}
@FilipMam
Tree of components
C
C
C
C
C
C
C
C
C
C
@FilipMam
Tree of components
V
V
V
V
V
V
V
V
V
V
views
@FilipMam
View object
view: View = {
component: Component = {...},
...
}
@FilipMam
Zone.js + Angular
class AppMain {
constructor(private zone: NgZone) {
this.zone.asyncEventDone
.subscribe(() => runAngularChangeDetection(this.rootView));
}
}
@FilipMam
Change detection order
@FilipMam
Change detection order
@FilipMam
Change detection order
@FilipMam
Change detection order
@FilipMam
Change detection order
@FilipMam
Change detection order
@FilipMam
View object
view: View = {
component: Component = {...},
oldValues: any[] = ["Luke", "Skywalker", "100"],
newValues: any[] = ["Luke", "Skywalker", "110"],
}
...
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 110
}
@FilipMam
Zone.js + Angular
class AppMain {
constructor(private zone: NgZone) {
this.zone.asyncEventDone
.subscribe(() => runAngularChangeDetection(this.rootView));
}
}
@FilipMam
 Change detection process
runAngularChangeDetection(view: View) {
view.oldValues.forEarch((value, i) => {
if (value !== view.newValues[i]) {
rerenderNewValue(i)
}
});
}
@FilipMam
 Change detection process
runAngularChangeDetection(view: View) {
view.oldValues.forEarch((value, i) => {
if (value !== view.newValues[i]) {
rerenderNewValue(i)
}
});
view.children.forEach(runAngularChangeDetection)
}
@FilipMam
DOM
["Luke", "Skywalker", "100"]
["Luke", "Skywalker", "110"]
@FilipMam
@FilipMam
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`})
class UserDetail implements AfterViewInit {
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
ngAfterViewInit() {
this.user.name = "Anakin";
}
}
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`})
class UserDetail implements AfterViewInit {
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
ngAfterViewInit() {
this.user.name = "Anakin";
// trigger change detection here
}
}
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`})
class UserDetail implements AfterViewInit {
constructor(cd: ChangeDetectorRef) {}
user: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
ngAfterViewInit() {
this.user.name = "Anakin";
this.cd.detectChanges();
}
}
@FilipMam
Angular's app structure
this.cd.detectChanges()
@FilipMam
Angular's app structure
this.cd.detectChanges()
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
Change detection process
runAngularChangeDetection(view: View) {
view.oldValues.forEarch((value, i) => {
if (value !== view.newValues[i]) {rerenderNewValue[i]}
});
view.children.forEach(runAngularChangeDetection)
}
onPush it to the limit
@FilipMam
@FilipMam
Changing CD strategy
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserDetail {
@Input();
user;
}
@FilipMam
Change detection process
runAngularChangeDetection(view: View) {
view.oldValues.forEarch((value, i) => {
if (value !== view.newValues[i]) {rerenderNewValue[i]}
});
view.children.forEach(runAngularChangeDetection)
}
@FilipMam
runAngularChangeDetection(view: View) {
if (view.checksEnabled) {
view.oldValues.forEarch((value, i) => {
if (value !== view.newValues[i]) {rerenderNewValue[i]}
});
view.children.forEach(runAngularChangeDetection)
}
}
Change detection process
@FilipMam
@Component({
changeDetection: ChangeDetectionStrategy.OnPush
})
view: View = {
checksEnabled: false
}
...
@Component({
changeDetection: ChangeDetectionStrategy.Default
})
view: View = {
checksEnabled: true
}
@FilipMam
runAngularChangeDetection(view: View) {
if (view.checksEnabled) {
checkProperties(view);
view.children.forEach(runAngularChangeDetection)
}
}
...
checkProperties(view: View) {
view.oldValues.forEarch((value, i) => {
if (value !== view.newValues[i]) {rerenderNewValue[i]}
});
}
Change detection process
@FilipMam
Tree of 'checkEnabled's
F
F
F
F
F
F
F
F
F
F
@FilipMam
Tree of 'checkEnabled's
F
F
F
F
F
F
F
F
F
F
@FilipMam
Tree of 'checkEnabled's
T
F
F
F
F
F
F
F
F
F
@FilipMam
runAngularChangeDetection(view: View) {
if (view.checksEnabled) {
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
}
Change detection process
@FilipMam
Tree of 'checkEnabled's
T
F
F
F
F
F
F
F
F
F
if (view.checksEnabled) { // false
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
if (view.checksEnabled) { // true
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
if (view.checksEnabled) { // false
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
if (view.checksEnabled) { // true
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
if (view.checksEnabled) { // true
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
if (view.checksEnabled) { // false
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
Changing model
-
Dom events
-
XHR requests
-
Timers
@FilipMam
-
Input changes
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
if (view.checksEnabled) { // ??
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
runAngularChangeDetection(view: View) {
if (view.checksEnabled) {
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
}
Change detection process
@FilipMam
runAngularChangeDetection(view: View) {
if (inputsValuesHaveChanged(view)) {
view.checksEnabled = true;
}
if (view.checksEnabled) {
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
}
Change detection process
@FilipMam
Check if inputs have changed
View {
oldInputsValues: any[];
inputsValues: any[];
}
@FilipMam
inputsValuesHaveChanged(view: View): boolean {
view.oldInputValues.any((oldValue, index) => {
return oldValue !== view.values[index];
})
}
Check if inputs have changed
@FilipMam
Changing input properties
@Component({
template: `
<user [user]="userFromParent"></user>
<button (click)="addMoney()">Add money</button>
`})
class ParentComponent {
userFromParent: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
addMoney() {
this.userFromParent.money += 10;
}
}
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
if (view.checksEnabled) { // ??
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
F
F
F
F
if (view.checksEnabled) { // false
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
inputsValuesHasChanged(view: View): boolean {
view.oldInputValues.any((oldValue, index) => {
return oldValue !== view.values[index];
})
}
Check if inputs have changed
@FilipMam
Changing input properties
@Component({
template: `
<user [user]="userFromParent"></user>
<button (click)="addMoney()">Add money</button>
`})
class ParentComponent {
userFromParent: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
addMoney() {
this.userFromParent.money += 10;
this.user = {...this.userFromParent, money: this.userFromParent.money + 10};
}
}
@FilipMam
Tree of 'checkEnabled's
T
T
T
F
F
F
T
F
F
F
if (view.checksEnabled) { // true
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
Changing input properties
@Component({
template: `
<user [user]="userFromParent"></user>
<button (click)="addMoney()">Add money</button>
`})
class ParentComponent {
userFromParent: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
addMoney() {
this.userFromParent.money += 10;
someImmutableApi.setValue(this.user.money, this.user.money + 10);
}
}
@FilipMam
@FilipMam
@FilipMam
@FilipMam
@FilipMam
S
@FilipMam
User service
@Injectable()
class UserService {
public user$: BevaviourSubject<User> = new BevaviourSubject<User>({
name: "Luke",
lastName: "Skywalker",
money: 100
});
public changeUser(): void {
this.user$.next({
name: "Obi Wan",
lastName: "Kenobi",
age: 2115
});
}
}
@FilipMam
User component + user service
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserDetail {
@Input();
user;
constructor(userService: UserService) {
userService.user$.subscribe(user => this.user = user);
}
}
@FilipMam
Parent component + user service
@Component({
template: `
<another-component></another-component>
<button (click)="addUser()">Add money</button>
`})
class ParentComponent {
constructor(private userService: UserService) {}
userFromParent: User = {
name: "Luke",
lastName: "Skywalker",
money: 100
}
addUser() {
this.userService.addUser();
}
}
@FilipMam
@FilipMam
S
@FilipMam
S
if (view.checksEnabled) { // false
checkProperties(view);
view.children.forEach(runAngularChangeDetection);
}
@FilipMam
S
@FilipMam
S
F
F
F
F
F
F
F
F
F
F
F
F
@FilipMam
S
F
F
F
T
T
F
F
F
F
F
F
F
@FilipMam
S
F
F
F
T
T
F
F
F
F
F
F
F
@FilipMam
S
F
F
F
T
T
T
T
T
F
F
F
T
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserDetail {
@Input();
user;
constructor(userService: UserService) {
userService.user$.subscribe(user => {
this.user = user;
}
}
}
User component + user service
@FilipMam
@Component({
selector: "user",
template: `
<span>{{user.name}}</span>
<strong>{{user.lastName}}</strong>
<span class="money">({{user.money}}$)</span>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
class UserDetail {
@Input();
user;
constructor(userService: UserService, cd: ChangeDetectorRef) {
userService.user$.subscribe(user => {
this.user = user;
cd.markForCheck();
}
}
}
User component + user service
@FilipMam
S
F
F
F
T
T
T
T
T
F
F
F
T
cd.markForCheck()
Recap
@FilipMam
CD boosts performance
@FilipMam
CD by default is stupid simple
@FilipMam
To use onPush:
- Provide metadata
- Make sure to change rererance of objects
@FilipMam
ChangeDetectorRef:
- detectChanges()
- markForCheck()
@FilipMam
Thanks!
@FilipMam
Change detection simplified - Angular Dragons #2
By Filip Mamcarczyk
Change detection simplified - Angular Dragons #2
- 549