Angular 的 Change Detection機制實現(xiàn)詳解
什么是 Change Detection ?
在應用的開發(fā)過程中,state 代表需要顯示在應用上的數(shù)據(jù)。當 state 發(fā)生變化時,往往需要一種機制來檢測變化的 state 并隨之更新對應的界面。這個機制就叫做 Change Detection 機制。
在 WEB 開發(fā)中,更新應用界面其實就是對 DOM 樹進行修改。由于 DOM 操作是昂貴的,所以一個效率低下的 Change Detection 會讓應用的性能變得很差。因此,框架在實現(xiàn) Change Detection 機制上的高效與否,很大程度上決定了其性能的好壞。
Change Detection 是如何實現(xiàn)的
Angular 可以檢測組件數(shù)據(jù)何時更改,然后自動重新渲染視圖以反映該更改。但是在像點擊按鈕這樣的低級事件之后,它怎么能做到這一點呢?
通過 Zone , Angular 能夠?qū)崿F(xiàn)自動的觸發(fā) Change Detection 機制。
Zone 是什么呢?簡而言之,Zone 是一個執(zhí)行上下文(execution context),可以理解為一個執(zhí)行環(huán)境。與常見的瀏覽器執(zhí)行環(huán)境不同,在這個環(huán)節(jié)中執(zhí)行的所有異步任務都被稱為 Task ,Zone 為這些 Task 提供了一堆的鉤子(hook),使得開發(fā)者可以很輕松的「監(jiān)控」環(huán)境中所有的異步任務。
題外話:由于 Angular 極力的推崇使用可觀察對象(Observable),如果完全的基于 Observable 來開發(fā)應用,可以代替 Zone 來實現(xiàn)追蹤調(diào)用棧的功能,且性能還比使用 Zone 會稍好一些。
// Angular 在 v5.0.0-beta.8 起可以通過配置不使用 Zone import { platformBrowser } from '@angular/platform-browser'; platformBrowser().bootstrapModuleFactory(AppModuleNgFactory, { ngZone: 'noop' });
覆蓋瀏覽器默認機制
Angular 在啟動時會重寫瀏覽器 low-level API,例如addEventListener
,它是用于注冊所有瀏覽器事件的瀏覽器函數(shù),包括點擊處理。Angular 將替換addEventListener
為與此等效的新版本:
// this is the new version of addEventListener function addEventListener(eventName, callback) { // call the real addEventListener callRealAddEventListener(eventName, function() { //first call the original callback callback(...); // and then run Angular-specific functionality var changed = angular.runChangeDetection(); if (changed) { angular.reRenderUIPart(); } }); }
新的addEventListener
為任何事件處理程序添加了更多功能:不僅調(diào)用了注冊的回調(diào),而且 Angular 有機會運行更改檢測并更新 UI。
支持瀏覽器異步 API
修補了以下常用瀏覽器機制以支持更改檢測:
- 所有瀏覽器事件(單擊、鼠標懸停、按鍵等)
setTimeout()
和setInterval()
- Ajax HTTP 請求
事實上,Zone.js 修補了許多其他瀏覽器 API,以透明地觸發(fā) Angular 更改檢測,例如 Websockets。
這種機制的一個限制是,如果由于某種原因 Zone.js 不支持的異步瀏覽器 API,則不會觸發(fā)更改檢測。例如,IndexedDB 回調(diào)就是這種情況。
默認的變更檢測機制是如何工作的?
每個 Angular 組件都有一個關(guān)聯(lián)的變更檢測器,它是在應用程序啟動時創(chuàng)建的。例如:
@Component({ selector: 'todo-item', template: `<span class="todo noselect" (click)="onToggle()">{{todo.owner.firstname}} - {{todo.description}} - completed: {{todo.completed}}</span>` }) export class TodoItem { @Input() todo:Todo; @Output() toggle = new EventEmitter<Object>(); onToggle() { this.toggle.emit(this.todo); } }
該組件將接收一個 Todo 對象作為輸入,并在 todo 狀態(tài)被切換時發(fā)出一個事件。
export class Todo { constructor(public id: number, public description: string, public completed: boolean, public owner: Owner) { } }
我們可以看到 Todo 有一個屬性owner
,它本身就是一個具有兩個屬性的對象:firstname
和lastname
。
變更檢測器是什么樣的?
我們實際上可以在運行時看到變化檢測器的樣子!要查看它,只需在 Todo 類中添加一些代碼以在訪問某個屬性時觸發(fā)斷點。
當斷點命中時,我們可以遍歷堆棧跟蹤并查看變化檢測:
這個方法一開始可能看起來很奇怪,所有變量都奇怪命名。但是通過深入研究,我們注意到它在做一些非常簡單的事情:對于模板中使用的每個表達式,它會將表達式中使用的屬性的當前值與該屬性的先前值進行比較。
如果前后的屬性值不同,就會設(shè)置isChanged
為true,就這樣!差不多,它是通過使用一個名為looseNotIdentical()
的方法來比較值。
那么嵌套對象owner
呢?
我們可以在更改檢測器代碼中看到 owner
嵌套對象的屬性也正在檢查差異。但只比較 firstname
屬性,而不是 lastname
屬性。這是因為組件template
中沒有使用lastname
!同樣,Todo 的頂級 id 屬性也沒有出于相同的原因進行比較。
有了這個,我們可以有把握地說:
默認情況下,Angular Change Detection 通過檢查模板表達式的值是否已更改來工作。
我們還可以得出結(jié)論:
默認情況下,Angular 不做深度對象比較來檢測變化,它只考慮模板使用的屬性
為什么默認情況下更改檢測會這樣工作?
Angular 的主要目標之一是更加透明和易于使用,因此框架用戶不必費盡心思調(diào)試框架并了解內(nèi)部機制即可有效地使用它。
如果 Angular 默認更改檢測機制基于組件輸入的參考比較而不是默認機制,那會是什么情況?即使是像 TODO 應用程序這樣簡單的東西也很難構(gòu)建:開發(fā)人員必須非常小心地創(chuàng)建一個新的 Todo,而不是簡單地更新屬性。
OnPush 變化檢測策略
如果你覺得默認模式影響了性能,我們也可以自定義 Angular 更改檢測。將組件更改檢測策略更新為OnPush
:
@Component({ selector: 'todo-list', changeDetection: ChangeDetectionStrategy.OnPush, template: ... }) export class TodoList { ... }
現(xiàn)在讓我們在應用程序中添加幾個按鈕:一個是通過直接改變列表的第一項來切換列表的第一項,另一個是向整個列表添加一個 Todo。代碼如下所示:
@Component({ selector: 'app', template: `<div> <todo-list [todos]="todos"></todo-list> </div> <button (click)="toggleFirst()">Toggle First Item</button> <button (click)="addTodo()">Add Todo to List</button>` }) export class App { todos:Array = initialData; constructor() { } toggleFirst() { this.todos[0].completed = ! this.todos[0].completed; } addTodo() { let newTodos = this.todos.slice(0); newTodos.push( new Todo(1, "TODO 4", false, new Owner("John", "Doe"))); this.todos = newTodos; } }
現(xiàn)在讓我們看看這兩個新按鈕的行為:
- 第一個按鈕“切換第一項”不起作用!這是因為該
toggleFirst()
方法直接改變了列表中的一個元素。TodoList
無法檢測到這一點,因為它的輸入?yún)⒖?code>todos沒有改變 - 第二個按鈕確實有效!請注意,該方法
addTodo()
創(chuàng)建了 todo 列表的副本,然后將項目添加到副本中,最后將 todos 成員變量替換為復制的列表。這會觸發(fā)更改檢測,因為組件檢測到其輸入中的參考更改:它收到了一個新列表! - 在第二個按鈕中,直接改變 todos 列表是行不通的!我們真的需要一個新的清單。
OnPush只是通過引用比較輸入嗎?
情況并非如此。
當使用 OnPush 檢測器時,框架將在 OnPush 組件的任何輸入屬性更改、觸發(fā)事件或 Observable 觸發(fā)事件時檢查
盡管允許更好的性能,但OnPush
如果與可變對象一起使用,則使用會帶來很高的復雜性成本。它可能會引入難以推理和重現(xiàn)的錯誤。但是有一種方法可以使使用OnPush
可行。
使用 Immutable.js 簡化 Angular 應用程序的構(gòu)建
如果我們只使用不可變對象和不可變列表來構(gòu)建我們的應用程序,則可以OnPush
透明地在任何地方使用,而不會遇到更改檢測錯誤的風險。這是因為對于不可變對象,修改數(shù)據(jù)的唯一方法是創(chuàng)建一個新的不可變對象并替換之前的對象。使用不可變對象,我們可以保證:
- 新的不可變對象將始終觸發(fā)
OnPush
更改檢測 - 我們不會因為忘記創(chuàng)建對象的新副本而意外創(chuàng)建錯誤,因為修改數(shù)據(jù)的唯一方法是創(chuàng)建新對象
實現(xiàn)不可變的一個不錯的選擇是使用Immutable.js庫。該庫為構(gòu)建應用程序提供了不可變原語,例如不可變對象(映射)和不可變列表。
避免變更檢測循環(huán):生產(chǎn)與開發(fā)模式
Angular 更改檢測的重要屬性之一是,與 AngularJs 不同,它強制執(zhí)行單向數(shù)據(jù)流:當我們的控制器類上的數(shù)據(jù)更新時,更改檢測運行并更新視圖。
如何在 Angular 中觸發(fā)變更檢測循環(huán)?
一種方法是如果我們使用生命周期回調(diào)。例如,在TodoList組件中,我們可以觸發(fā)對另一個組件的回調(diào)來更改其中一個綁定:
ngAfterViewChecked() { if (this.callback && this.clicked) { console.log("changing status ..."); this.callback(Math.random()); } }
控制臺中將顯示一條錯誤消息:
EXCEPTION: Expression '{{message}} in App@3:20' has changed after it was checked
僅當我們在開發(fā)模式下運行 Angular 時才會拋出此錯誤消息。如果我們啟用生產(chǎn)模式會發(fā)生什么? 在生產(chǎn)模式下,錯誤不會被拋出,問題也不會被發(fā)現(xiàn)。
在開發(fā)階段始終使用開發(fā)模式會更好,因為這樣可以避免問題。這種保證是以 Angular 總是運行兩次變更檢測為代價的,第二次檢測這種情況。在生產(chǎn)模式下,變更檢測只運行一次。
打開/關(guān)閉變化檢測,并手動觸發(fā)它
在某些特殊情況下,我們確實想要關(guān)閉更改檢測。想象一下這樣一種情況,大量數(shù)據(jù)通過 websocket 從后端到達。我們可能只想每 5 秒更新一次 UI 的某個部分。為此,我們首先將更改檢測器注入到組件中:
constructor(private ref: ChangeDetectorRef) { ref.detach(); setInterval(() => { this.ref.detectChanges(); }, 5000); }
正如我們所看到的,我們只是分離了變化檢測器,這有效地關(guān)閉了變化檢測。然后我們只需每 5 秒通過調(diào)用手動觸發(fā)它detectChanges()
。
現(xiàn)在讓我們快速總結(jié)一下我們需要了解的關(guān)于 Angular 變更檢測的所有內(nèi)容:它是什么,它是如何工作的以及可用的主要變更檢測類型是什么。
概括
Angular 更改檢測是一個內(nèi)置的框架功能,可確保組件數(shù)據(jù)與其 HTML 模板視圖之間的自動同步。
更改檢測的工作原理是檢測常見的瀏覽器事件,如鼠標點擊、HTTP 請求和其他類型的事件,并確定每個組件的視圖是否需要更新。
變更檢測有兩種類型:
- 默認更改檢測:Angular 通過比較事件發(fā)生前后的所有模板表達式值來決定是否需要更新視圖,用于組件樹的所有組件
- OnPush 更改檢測:這通過檢測是否已通過組件輸入或使用異步管道訂閱的 Observable 將某些新數(shù)據(jù)顯式推送到組件中來工作
Angular默認更改檢測機制實際上與 AngularJs 非常相似:它比較瀏覽器事件之前和之后模板表達式的值,以查看是否有更改。它對所有組件都這樣做。但也有一些重要的區(qū)別:
一方面,沒有變化檢測循環(huán),也沒有 AngularJs 中命名的摘要循環(huán)。這允許僅通過查看其模板和控制器來推理每個組件。
另一個區(qū)別是,由于變化檢測器的構(gòu)建方式,檢測組件變化的機制要快得多。
最后,與 AngularJs 不同的是,變化檢測機制是可定制的。
以上就是Angular 的 Change Detection機制實現(xiàn)詳解的詳細內(nèi)容,更多關(guān)于Angular Change Detection機制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
AngularJS基礎(chǔ) ng-paste 指令簡單示例
本文主要介紹AngularJS ng-paste 指令,這里對ng-paste 指令的基礎(chǔ)資料做了整理,并附有代碼示例,有需要的朋友可以參考下2016-08-08AngularJS基礎(chǔ) ng-mouseleave 指令詳解
本文主要介紹AngularJS ng-mouseleave 指令,這里幫大家整理了ng-mouseleave指令的詳細資料,并附有代碼示例,有需要的小伙伴可以參考下2016-08-08Ionic+AngularJS實現(xiàn)登錄和注冊帶驗證功能
這篇文章主要介紹了Ionic+AngularJS實現(xiàn)登錄和注冊帶驗證功能,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-02-02AngularJS 將再發(fā)布一個重要版本 然后進入長期支持階段
目前團隊正在開發(fā) AngularJS 1.7.0,而 1.7 的開發(fā)周期將一直持續(xù)到 2018 年 6 月 30 日2018-01-01Angular+Ionic使用queryParams實現(xiàn)跳轉(zhuǎn)頁傳值的方法
這篇文章主要介紹了Angular+Ionic使用queryParams實現(xiàn)跳轉(zhuǎn)頁傳值的方法,本文給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09angular內(nèi)置provider之$compileProvider詳解
下面小編就為大家?guī)硪黄猘ngular內(nèi)置provider之$compileProvider詳解。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-09-09淺析Angular 實現(xiàn)一個repeat指令的方法
這篇文章主要介紹了Angular 實現(xiàn)一個repeat指令的方法,本文給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2019-07-07