[譯]深刻理解Angular onPush變動檢測策略

原文連接: A Comprehensive Guide to Angular onPush Change Detection Strategyjavascript

原文做者:Netanel Basalhtml

譯者:淼淼java

默認的變動檢測策略

默認狀況下,Angular使用ChangeDetectionStrategy.Default策略來進行變動檢測。ajax

默認策略並不事先對應用作出任何假設,所以,每當用戶事件、記時器、XHR、promise等事件使應用中的數據將發生了改變時,全部的組件中都會執行變動檢測。promise

這意味着從點擊事件到從ajax調用接收到的數據之類的任何事件都會觸發更改檢測。bash

經過在組件中定義一個getter而且在模板中使用它,咱們能夠很容易的看出這一點:app

@Component({
  template: ` <h1>Hello {{name}}!</h1> {{runChangeDetection}} `
})
export class HelloComponent {
  @Input() name: string;

  get runChangeDetection() {
    console.log('Checking the view');
    return true;
  }
}
複製代碼
@Component({
  template: ` <hello></hello> <button (click)="onClick()">Trigger change detection</button> `
})
export class AppComponent  {
  onClick() {}
}
複製代碼

執行以上代碼後,每當咱們點擊按鈕時。Angular將會執行一遍變動檢測循環,在console裏咱們能夠看到兩行「Checking the view」的日誌。dom

這種技術被稱做髒檢查。爲了知道視圖是否須要更新,Angular須要訪問新值並和舊值比較來判斷是否須要更新視圖。異步

如今想象一下,若是有一個有成千上萬個表達式的大應用,Angular去檢查每個表達式,咱們可能會遇到性能上的問題。async

那麼有沒有辦法讓咱們主動告訴Angular何時去檢查咱們的組件呢?

OnPush變動檢測策略

咱們能夠將組件的ChangeDetectionStrategy設置成ChangeDetectionStrategy.OnPush

這將告訴Angular該組件僅僅依賴於它的@inputs(),只有在如下幾種狀況才須要檢查:

1. Input引用發生改變

經過設置onPush變動檢測測策略,咱們與Angular約定強制使用不可變對象(或稍後將要介紹的observables)。

在變動檢測的上下文中使用不可變對象的好處是,Angular能夠經過檢查引用是否發生了改變來判斷視圖是否須要檢查。這將會比深度檢查要容易不少。

讓咱們試試來修改一個對象而後看看結果。

@Component({
  selector: 'tooltip',
  template: ` <h1>{{config.position}}</h1> {{runChangeDetection}} `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent  {

  @Input() config;

  get runChangeDetection() {
    console.log('Checking the view');
    return true;
  }
}
複製代碼
@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: 'top'
  };

  onClick() {
    this.config.position = 'bottom';
  }
}
複製代碼

這時候去點擊按鈕時看不到任何日誌了,這是由於Angular將舊值和新值的引用進行比較,相似於:

/** Returns false in our case */
if( oldValue !== newValue ) { 
  runChangeDetection();
}
複製代碼

值得一提的是numbers, booleans, strings, null 、undefined都是原始類型。全部的原始類型都是按值傳遞的. Objects, arrays, 還有 functions 也是按值傳遞的,只不過值是引用地址的副本

因此爲了觸發對該組件的變動檢測,咱們須要更改這個object的引用。

@Component({
  template: ` <tooltip [config]="config"></tooltip> `
})
export class AppComponent  {
  config = {
    position: 'top'
  };

  onClick() {
    this.config = {
      position: 'bottom'
    }
  }
}
複製代碼

將對象引用改變後,咱們將看到視圖已被檢查,新值被展現出來。

2.源於該組件或其子組件的事件

當在一個組件或者其子組件中觸發了某一個事件時,這個組件的內部狀態會更新。 例如:

@Component({
  template: ` <button (click)="add()">Add</button> {{count}} `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;

  add() {
    this.count++;
  }

}
複製代碼

當咱們點擊按鈕時,Angular執行變動檢測循環並更新視圖。

你可能會想,按照咱們開頭講述的那樣,每一次異步的API都會觸發變動檢測,可是並非這樣。

你會發現這個規則只適用於DOM事件,下面這些API並不會觸發變動檢測:

@Component({
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;

  constructor() {
    setTimeout(() => this.count = 5, 0);

    setInterval(() => this.count = 5, 100);

    Promise.resolve().then(() => this.count = 5); 
    
    this.http.get('https://count.com').subscribe(res => {
      this.count = res;
    });
  }

  add() {
    this.count++;
  }
複製代碼

注意你仍然是更新了該屬性的,因此在下一個變動檢測流程中,好比去點擊按鈕,count值將會變成6(5+1)。

3. 顯示的去執行變動檢測

Angular給咱們提供了3種方法來觸發變動檢測。

第一個是detectChanges()來告訴Angular在該組件和它的子組件中去執行變動檢測。

@Component({
  selector: 'counter',
  template: `{{count}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent { 
  count = 0;

  constructor(private cdr: ChangeDetectorRef) {

    setTimeout(() => {
      this.count = 5;
      this.cdr.detectChanges();
    }, 1000);

  }

}
複製代碼

第二個是ApplicationRef.tick(),它告訴Angular來對整個應用程序執行變動檢測。

tick() {
 
  try {
    this._views.forEach((view) => view.detectChanges());
    ...
  } catch (e) {
    ...
  }
}
複製代碼

第三是markForCheck(),它不會觸發變動檢測。相反,它會將全部設置了onPush的祖先標記,在當前或者下一次變動檢測循環中檢測。

markForCheck(): void { 
  markParentViewsForCheck(this._view); 
}

export function markParentViewsForCheck(view: ViewData) {
  let currView: ViewData|null = view;
  while (currView) {
    if (currView.def.flags & ViewFlags.OnPush) {
      currView.state |= ViewState.ChecksEnabled;
    }
    currView = currView.viewContainerParent || currView.parent;
  }
}
複製代碼

須要注意的是,手動執行變動檢測並非一種「hack」,這是Angular有意的設計而且是很是合理的行爲(固然,是在合理的場景下)。

🤓 Angular Async pipe

async pipe會訂閱一個 Observable 或 Promise,並返回它發出的最近一個值。

讓咱們看一個input()是observable的onPush組件。

@Component({
  template: ` <button (click)="add()">Add</button> <app-list [items$]="items$"></app-list> `
})
export class AppComponent {
  items = [];
  items$ = new BehaviorSubject(this.items);

  add() {
    this.items.push({ title: Math.random() })
    this.items$.next(this.items);
  }
}
複製代碼
@Component({
  template: ` <div *ngFor="let item of _items ; ">{{item.title}}</div> `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items: Observable<Item>;
  _items: Item[];
  
  ngOnInit() {
    this.items.subscribe(items => {
      this._items = items;
    });
  }

}
複製代碼

當咱們點擊按鈕並不能看到視圖更新。這是由於上述提到的幾種狀況均未發生,因此Angular在當前變動檢測循環並不會檢車該組件。

如今,讓咱們加上async pipe試試。

@Component({
  template: ` <div *ngFor="let item of items | async">{{item.title}}</div> `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items;
}
複製代碼

如今能夠看到當咱們點擊按鈕時,視圖也更新了。緣由是當新的值被髮射出來時,async pipe將該組件標記爲發生了更改須要檢查。咱們能夠在源碼中看到:

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}
複製代碼

Angular爲咱們調用markForCheck(),因此咱們能看到視圖更新了即便input的引用沒有發生改變。

若是一個組件僅僅依賴於它的input屬性,而且input屬性是observable,那麼這個組件只有在它的input屬性發射一個事件的時候纔會發生改變。

Quick tip:對外部暴露你的subject是不值得提倡的,老是使用asObservable()方法來暴露該observable。

👀 onPush和視圖查詢

@Component({
  selector: 'app-tabs',
  template: `<ng-content></ng-content>`
})
export class TabsComponent implements OnInit {
  @ContentChild(TabComponent) tab: TabComponent;

  ngAfterContentInit() {
    setTimeout(() => {
      this.tab.content = 'Content'; 
    }, 3000);
  }
}
複製代碼
@Component({
  selector: 'app-tab',
  template: `{{content}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  @Input() content;
}
複製代碼
<app-tabs>
  <app-tab></app-tab>
</app-tabs>
複製代碼

也許你會覺得3秒後Angular將會使用新的內容更新tab組件。

畢竟,咱們更新來onPush組件的input引用,這將會觸發變動檢測不是嗎?

然而,在這種狀況下,它並不生效。Angular不知道咱們正在更新tab組件的input屬性,在模板中定義input()是讓Angular知道應在變動檢測循環中檢查此屬性的惟一途徑。

例如:

<app-tabs>
  <app-tab [content]="content"></app-tab>
</app-tabs>
複製代碼

由於當咱們明確的在模板中定義了input(),Angular會建立一個叫updateRenderer()的方法,它會在每一個變動檢測循環中都對content的值進行追蹤。

AppComponent.ngfactory.ts

在這種狀況下簡單的解決辦法使用setter而後調用markForCheck()

@Component({
  selector: 'app-tab',
  template: ` {{_content}} `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  _content;

  @Input() set content(value) {
    this._content = value;
    this.cdr.markForCheck();
  }

  constructor(private cdr: ChangeDetectorRef) {}

}
複製代碼

💪 === onPush++

在理解了onPush的強大以後,咱們來利用它創造一個更高性能的應用。onPush組件越多,Angular須要執行的檢查就越少。讓咱們看看你一個真是的例子:

咱們又一個todos組件,它有一個todos做爲input()。

@Component({
  selector: 'app-todos',
  template: ` <div *ngFor="let todo of todos"> {{todo.title}} - {{runChangeDetection}} </div> `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos;

  get runChangeDetection() {
    console.log('TodosComponent - Checking the view');
    return true;
  }

}
複製代碼
@Component({
  template: ` <button (click)="add()">Add</button> <app-todos [todos]="todos"></app-todos> `
})
export class AppComponent {
  todos = [{ title: 'One' }, { title: 'Two' }];

  add() {
    this.todos = [...this.todos, { title: 'Three' }];
  }
}
複製代碼

上述方法的缺點是,當咱們單擊添加按鈕時,即便以前的數據沒有任何更改,Angular也須要檢查每一個todo。所以第一次單擊後,控制檯中將顯示三個日誌。

在上面的示例中,只有一個表達式須要檢查,可是想象一下若是是一個有多個綁定(ngIf,ngClass,表達式等)的真實組件,這將會很是耗性能。

咱們白白的執行了變動檢測!

更高效的方法是建立一個todo組件並將其變動檢測策略定義爲onPush。例如:

@Component({
  selector: 'app-todos',
  template: ` <app-todo [todo]="todo" *ngFor="let todo of todos"></app-todo> `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos;
}

@Component({
  selector: 'app-todo',
  template: `{{todo.title}} {{runChangeDetection}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
  @Input() todo;

  get runChangeDetection() {
    console.log('TodoComponent - Checking the view');
    return true;
  }

}
複製代碼

如今,當咱們單擊添加按鈕時,控制檯中只會看到一個日誌,由於其餘的todo組件的input均未更改,所以不會去檢查其視圖。

而且,經過建立更小粒度的組件,咱們的代碼變得更具可讀性和可重用性。

相關文章
相關標籤/搜索