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:

  1. Provide metadata
  2. 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

  • 557