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
- 637