您好,登錄后才能下訂單哦!
這篇文章給大家介紹Angular中的onPush變更檢測策略有哪些,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
默認情況下,Angular使用ChangeDetectionStrategy.Default
策略來進行變更檢測。
默認策略并不事先對應用做出任何假設,因此,每當用戶事件、記時器、XHR、promise等事件使應用中的數據將發生了改變時,所有的組件中都會執行變更檢測。
這意味著從點擊事件到從ajax調用接收到的數據之類的任何事件都會觸發更改檢測。
通過在組件中定義一個getter并且在模板中使用它,我們可以很容易的看出這一點:
@Component({ template: ` <h2>Hello {{name}}!</h2> {{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: ` <h2>{{config.position}}</h2> {{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均未更改,因此不會去檢查其視圖。
并且,通過創建更小粒度的組件,我們的代碼變得更具可讀性和可重用性。
關于Angular中的onPush變更檢測策略有哪些就分享到這里了,希望以上內容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。