這篇文章帶大家深入了解Angular中的onPush變更偵測策略,希望對大家有幫助!
#預設情況下,Angular使用ChangeDetectionStrategy.Default
策略來進行變更檢測。
預設策略並未事先對應用程式做出任何假設,因此,每當使用者事件、記時器、XHR、promise等事件使應用程式中的資料將發生了改變時,所有的元件中都會執行變更檢測。
這表示從點擊事件到從ajax呼叫接收到的資料之類的任何事件都會觸發變更偵測。
透過在元件中定義一個getter並且在模板中使用它,我們可以很容易的看出這一點:
@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」的日誌。
這種技術被稱為髒檢查。為了知道視圖是否需要更新,Angular需要存取新值並和舊值比較來判斷是否需要更新視圖。
現在想像一下,如果有一個有成千上萬個表達式的大應用,Angular去檢查每一個表達式,我們可能會遇到效能上的問題。
那麼有沒有辦法讓我們主動告訴Angular什麼時候去檢查我們的元件呢?
我們可以將元件的ChangeDetectionStrategy
設定成ChangeDetectionStrategy.OnPush
。
這將告訴Angular該元件僅依賴它的@inputs()
,只有在以下幾種情況才需要檢查:
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' } } }
將物件參考改變後,我們將看到視圖已被檢查,新值被顯示出來。
當在一個元件或其子元件中觸發了某一個事件時,這個元件的內部狀態會更新。 例如:
@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)。
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有意的設計並且是非常合理的行為(當然,是在合理的場景下)。
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。
@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的值进行追踪。
在这种情况下简单的解决办法使用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组件越多,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均未更改,因此不会去检查其视图。
并且,通过创建更小粒度的组件,我们的代码变得更具可读性和可重用性。
原文链接: https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4
原文作者:Netanel Basal
译者:淼淼
更多编程相关知识,请访问:编程视频!!
以上是快速了解Angular中的onPush變更檢測策略的詳細內容。更多資訊請關注PHP中文網其他相關文章!