Angular 的 Change Detection機(jī)制實(shí)現(xiàn)詳解
什么是 Change Detection ?
在應(yīng)用的開發(fā)過(guò)程中,state 代表需要顯示在應(yīng)用上的數(shù)據(jù)。當(dāng) state 發(fā)生變化時(shí),往往需要一種機(jī)制來(lái)檢測(cè)變化的 state 并隨之更新對(duì)應(yīng)的界面。這個(gè)機(jī)制就叫做 Change Detection 機(jī)制。
在 WEB 開發(fā)中,更新應(yīng)用界面其實(shí)就是對(duì) DOM 樹進(jìn)行修改。由于 DOM 操作是昂貴的,所以一個(gè)效率低下的 Change Detection 會(huì)讓應(yīng)用的性能變得很差。因此,框架在實(shí)現(xiàn) Change Detection 機(jī)制上的高效與否,很大程度上決定了其性能的好壞。
Change Detection 是如何實(shí)現(xiàn)的
Angular 可以檢測(cè)組件數(shù)據(jù)何時(shí)更改,然后自動(dòng)重新渲染視圖以反映該更改。但是在像點(diǎn)擊按鈕這樣的低級(jí)事件之后,它怎么能做到這一點(diǎn)呢?
通過(guò) Zone , Angular 能夠?qū)崿F(xiàn)自動(dòng)的觸發(fā) Change Detection 機(jī)制。
Zone 是什么呢?簡(jiǎn)而言之,Zone 是一個(gè)執(zhí)行上下文(execution context),可以理解為一個(gè)執(zhí)行環(huán)境。與常見的瀏覽器執(zhí)行環(huán)境不同,在這個(gè)環(huán)節(jié)中執(zhí)行的所有異步任務(wù)都被稱為 Task ,Zone 為這些 Task 提供了一堆的鉤子(hook),使得開發(fā)者可以很輕松的「監(jiān)控」環(huán)境中所有的異步任務(wù)。
題外話:由于 Angular 極力的推崇使用可觀察對(duì)象(Observable),如果完全的基于 Observable 來(lái)開發(fā)應(yīng)用,可以代替 Zone 來(lái)實(shí)現(xiàn)追蹤調(diào)用棧的功能,且性能還比使用 Zone 會(huì)稍好一些。
// Angular 在 v5.0.0-beta.8 起可以通過(guò)配置不使用 Zone
import { platformBrowser } from '@angular/platform-browser';
platformBrowser().bootstrapModuleFactory(AppModuleNgFactory, { ngZone: 'noop' });
覆蓋瀏覽器默認(rèn)機(jī)制
Angular 在啟動(dòng)時(shí)會(huì)重寫瀏覽器 low-level API,例如addEventListener,它是用于注冊(cè)所有瀏覽器事件的瀏覽器函數(shù),包括點(diǎn)擊處理。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)用了注冊(cè)的回調(diào),而且 Angular 有機(jī)會(huì)運(yùn)行更改檢測(cè)并更新 UI。
支持瀏覽器異步 API
修補(bǔ)了以下常用瀏覽器機(jī)制以支持更改檢測(cè):
- 所有瀏覽器事件(單擊、鼠標(biāo)懸停、按鍵等)
setTimeout()和setInterval()- Ajax HTTP 請(qǐng)求
事實(shí)上,Zone.js 修補(bǔ)了許多其他瀏覽器 API,以透明地觸發(fā) Angular 更改檢測(cè),例如 Websockets。
這種機(jī)制的一個(gè)限制是,如果由于某種原因 Zone.js 不支持的異步瀏覽器 API,則不會(huì)觸發(fā)更改檢測(cè)。例如,IndexedDB 回調(diào)就是這種情況。
默認(rèn)的變更檢測(cè)機(jī)制是如何工作的?
每個(gè) Angular 組件都有一個(gè)關(guān)聯(lián)的變更檢測(cè)器,它是在應(yīng)用程序啟動(dòng)時(shí)創(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);
}
}
該組件將接收一個(gè) Todo 對(duì)象作為輸入,并在 todo 狀態(tài)被切換時(shí)發(fā)出一個(gè)事件。
export class Todo {
constructor(public id: number,
public description: string,
public completed: boolean,
public owner: Owner) {
}
}
我們可以看到 Todo 有一個(gè)屬性owner,它本身就是一個(gè)具有兩個(gè)屬性的對(duì)象:firstname和lastname。
變更檢測(cè)器是什么樣的?
我們實(shí)際上可以在運(yùn)行時(shí)看到變化檢測(cè)器的樣子!要查看它,只需在 Todo 類中添加一些代碼以在訪問(wèn)某個(gè)屬性時(shí)觸發(fā)斷點(diǎn)。
當(dāng)斷點(diǎn)命中時(shí),我們可以遍歷堆棧跟蹤并查看變化檢測(cè):

這個(gè)方法一開始可能看起來(lái)很奇怪,所有變量都奇怪命名。但是通過(guò)深入研究,我們注意到它在做一些非常簡(jiǎn)單的事情:對(duì)于模板中使用的每個(gè)表達(dá)式,它會(huì)將表達(dá)式中使用的屬性的當(dāng)前值與該屬性的先前值進(jìn)行比較。
如果前后的屬性值不同,就會(huì)設(shè)置isChanged 為true,就這樣!差不多,它是通過(guò)使用一個(gè)名為looseNotIdentical() 的方法來(lái)比較值。
那么嵌套對(duì)象owner呢?
我們可以在更改檢測(cè)器代碼中看到 owner 嵌套對(duì)象的屬性也正在檢查差異。但只比較 firstname 屬性,而不是 lastname 屬性。這是因?yàn)榻M件template中沒(méi)有使用lastname!同樣,Todo 的頂級(jí) id 屬性也沒(méi)有出于相同的原因進(jìn)行比較。
有了這個(gè),我們可以有把握地說(shuō):
默認(rèn)情況下,Angular Change Detection 通過(guò)檢查模板表達(dá)式的值是否已更改來(lái)工作。
我們還可以得出結(jié)論:
默認(rèn)情況下,Angular 不做深度對(duì)象比較來(lái)檢測(cè)變化,它只考慮模板使用的屬性
為什么默認(rèn)情況下更改檢測(cè)會(huì)這樣工作?
Angular 的主要目標(biāo)之一是更加透明和易于使用,因此框架用戶不必費(fèi)盡心思調(diào)試框架并了解內(nèi)部機(jī)制即可有效地使用它。
如果 Angular 默認(rèn)更改檢測(cè)機(jī)制基于組件輸入的參考比較而不是默認(rèn)機(jī)制,那會(huì)是什么情況?即使是像 TODO 應(yīng)用程序這樣簡(jiǎn)單的東西也很難構(gòu)建:開發(fā)人員必須非常小心地創(chuàng)建一個(gè)新的 Todo,而不是簡(jiǎn)單地更新屬性。
OnPush 變化檢測(cè)策略
如果你覺(jué)得默認(rèn)模式影響了性能,我們也可以自定義 Angular 更改檢測(cè)。將組件更改檢測(cè)策略更新為OnPush:
@Component({
selector: 'todo-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: ...
})
export class TodoList {
...
}
現(xiàn)在讓我們?cè)趹?yīng)用程序中添加幾個(gè)按鈕:一個(gè)是通過(guò)直接改變列表的第一項(xiàng)來(lái)切換列表的第一項(xiàng),另一個(gè)是向整個(gè)列表添加一個(gè) 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)在讓我們看看這兩個(gè)新按鈕的行為:
- 第一個(gè)按鈕“切換第一項(xiàng)”不起作用!這是因?yàn)樵?code>toggleFirst()方法直接改變了列表中的一個(gè)元素。
TodoList無(wú)法檢測(cè)到這一點(diǎn),因?yàn)樗妮斎雲(yún)⒖?code>todos沒(méi)有改變 - 第二個(gè)按鈕確實(shí)有效!請(qǐng)注意,該方法
addTodo()創(chuàng)建了 todo 列表的副本,然后將項(xiàng)目添加到副本中,最后將 todos 成員變量替換為復(fù)制的列表。這會(huì)觸發(fā)更改檢測(cè),因?yàn)榻M件檢測(cè)到其輸入中的參考更改:它收到了一個(gè)新列表! - 在第二個(gè)按鈕中,直接改變 todos 列表是行不通的!我們真的需要一個(gè)新的清單。
OnPush只是通過(guò)引用比較輸入嗎?
情況并非如此。
當(dāng)使用 OnPush 檢測(cè)器時(shí),框架將在 OnPush 組件的任何輸入屬性更改、觸發(fā)事件或 Observable 觸發(fā)事件時(shí)檢查
盡管允許更好的性能,但OnPush如果與可變對(duì)象一起使用,則使用會(huì)帶來(lái)很高的復(fù)雜性成本。它可能會(huì)引入難以推理和重現(xiàn)的錯(cuò)誤。但是有一種方法可以使使用OnPush可行。
使用 Immutable.js 簡(jiǎn)化 Angular 應(yīng)用程序的構(gòu)建
如果我們只使用不可變對(duì)象和不可變列表來(lái)構(gòu)建我們的應(yīng)用程序,則可以OnPush透明地在任何地方使用,而不會(huì)遇到更改檢測(cè)錯(cuò)誤的風(fēng)險(xiǎn)。這是因?yàn)閷?duì)于不可變對(duì)象,修改數(shù)據(jù)的唯一方法是創(chuàng)建一個(gè)新的不可變對(duì)象并替換之前的對(duì)象。使用不可變對(duì)象,我們可以保證:
- 新的不可變對(duì)象將始終觸發(fā)
OnPush更改檢測(cè) - 我們不會(huì)因?yàn)橥泟?chuàng)建對(duì)象的新副本而意外創(chuàng)建錯(cuò)誤,因?yàn)樾薷臄?shù)據(jù)的唯一方法是創(chuàng)建新對(duì)象
實(shí)現(xiàn)不可變的一個(gè)不錯(cuò)的選擇是使用Immutable.js庫(kù)。該庫(kù)為構(gòu)建應(yīng)用程序提供了不可變?cè)Z(yǔ),例如不可變對(duì)象(映射)和不可變列表。
避免變更檢測(cè)循環(huán):生產(chǎn)與開發(fā)模式
Angular 更改檢測(cè)的重要屬性之一是,與 AngularJs 不同,它強(qiáng)制執(zhí)行單向數(shù)據(jù)流:當(dāng)我們的控制器類上的數(shù)據(jù)更新時(shí),更改檢測(cè)運(yùn)行并更新視圖。
如何在 Angular 中觸發(fā)變更檢測(cè)循環(huán)?
一種方法是如果我們使用生命周期回調(diào)。例如,在TodoList組件中,我們可以觸發(fā)對(duì)另一個(gè)組件的回調(diào)來(lái)更改其中一個(gè)綁定:
ngAfterViewChecked() {
if (this.callback && this.clicked) {
console.log("changing status ...");
this.callback(Math.random());
}
}
控制臺(tái)中將顯示一條錯(cuò)誤消息:
EXCEPTION: Expression '{{message}} in App@3:20' has changed after it was checked
僅當(dāng)我們?cè)陂_發(fā)模式下運(yùn)行 Angular 時(shí)才會(huì)拋出此錯(cuò)誤消息。如果我們啟用生產(chǎn)模式會(huì)發(fā)生什么? 在生產(chǎn)模式下,錯(cuò)誤不會(huì)被拋出,問(wèn)題也不會(huì)被發(fā)現(xiàn)。
在開發(fā)階段始終使用開發(fā)模式會(huì)更好,因?yàn)檫@樣可以避免問(wèn)題。這種保證是以 Angular 總是運(yùn)行兩次變更檢測(cè)為代價(jià)的,第二次檢測(cè)這種情況。在生產(chǎn)模式下,變更檢測(cè)只運(yùn)行一次。
打開/關(guān)閉變化檢測(cè),并手動(dòng)觸發(fā)它
在某些特殊情況下,我們確實(shí)想要關(guān)閉更改檢測(cè)。想象一下這樣一種情況,大量數(shù)據(jù)通過(guò) websocket 從后端到達(dá)。我們可能只想每 5 秒更新一次 UI 的某個(gè)部分。為此,我們首先將更改檢測(cè)器注入到組件中:
constructor(private ref: ChangeDetectorRef) {
ref.detach();
setInterval(() => {
this.ref.detectChanges();
}, 5000);
}
正如我們所看到的,我們只是分離了變化檢測(cè)器,這有效地關(guān)閉了變化檢測(cè)。然后我們只需每 5 秒通過(guò)調(diào)用手動(dòng)觸發(fā)它detectChanges()。
現(xiàn)在讓我們快速總結(jié)一下我們需要了解的關(guān)于 Angular 變更檢測(cè)的所有內(nèi)容:它是什么,它是如何工作的以及可用的主要變更檢測(cè)類型是什么。
概括
Angular 更改檢測(cè)是一個(gè)內(nèi)置的框架功能,可確保組件數(shù)據(jù)與其 HTML 模板視圖之間的自動(dòng)同步。
更改檢測(cè)的工作原理是檢測(cè)常見的瀏覽器事件,如鼠標(biāo)點(diǎn)擊、HTTP 請(qǐng)求和其他類型的事件,并確定每個(gè)組件的視圖是否需要更新。
變更檢測(cè)有兩種類型:
- 默認(rèn)更改檢測(cè):Angular 通過(guò)比較事件發(fā)生前后的所有模板表達(dá)式值來(lái)決定是否需要更新視圖,用于組件樹的所有組件
- OnPush 更改檢測(cè):這通過(guò)檢測(cè)是否已通過(guò)組件輸入或使用異步管道訂閱的 Observable 將某些新數(shù)據(jù)顯式推送到組件中來(lái)工作
Angular默認(rèn)更改檢測(cè)機(jī)制實(shí)際上與 AngularJs 非常相似:它比較瀏覽器事件之前和之后模板表達(dá)式的值,以查看是否有更改。它對(duì)所有組件都這樣做。但也有一些重要的區(qū)別:
一方面,沒(méi)有變化檢測(cè)循環(huán),也沒(méi)有 AngularJs 中命名的摘要循環(huán)。這允許僅通過(guò)查看其模板和控制器來(lái)推理每個(gè)組件。
另一個(gè)區(qū)別是,由于變化檢測(cè)器的構(gòu)建方式,檢測(cè)組件變化的機(jī)制要快得多。
最后,與 AngularJs 不同的是,變化檢測(cè)機(jī)制是可定制的。
以上就是Angular 的 Change Detection機(jī)制實(shí)現(xiàn)詳解的詳細(xì)內(nèi)容,更多關(guān)于Angular Change Detection機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
測(cè)試IE瀏覽器對(duì)JavaScript的AngularJS的兼容性
這篇文章主要介紹了測(cè)試IE瀏覽器對(duì)JavaScript的AngularJS的兼容性的方法,盡管隨著Windows10的近期上市,IE瀏覽器即將成為歷史...需要的朋友可以參考下2015-06-06
詳解AngularJS臟檢查機(jī)制及$timeout的妙用
本篇文章主要介紹了詳解AngularJS臟檢查機(jī)制及$timeout的妙用,“臟檢查”是Angular中的核心機(jī)制之一,它是實(shí)現(xiàn)雙向綁定、MVVM模式的重要基礎(chǔ),有興趣的可以了解一下2017-06-06
AngularJS使用ng-repeat遍歷二維數(shù)組元素的方法詳解
這篇文章主要介紹了AngularJS使用ng-repeat遍歷二維數(shù)組元素的方法,結(jié)合實(shí)例形式分析了AngularJS二維數(shù)組元素遍歷的相關(guān)操作技巧,需要的朋友可以參考下2017-11-11
AngularJS使用ui-route實(shí)現(xiàn)多層嵌套路由的示例
這篇文章主要介紹了AngularJS使用ui-route實(shí)現(xiàn)多層嵌套路由的示例,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
使用AngularJS中的SCE來(lái)防止XSS攻擊的方法
這篇文章主要介紹了使用AngularJS中的SCE來(lái)防止XSS攻擊的方法,依靠合理地轉(zhuǎn)碼為HTML來(lái)預(yù)防跨站腳本漏洞共計(jì),需要的朋友可以參考下2015-06-06
AngularJs基于角色的前端訪問(wèn)控制的實(shí)現(xiàn)
本篇文章主要介紹了AngularJs實(shí)現(xiàn)基于角色的前端訪問(wèn)控制,可以適用于不同的角色,有需要的可以了解一下。2016-11-11
對(duì)angularJs中controller控制器scope父子集作用域的實(shí)例講解
今天小編就為大家分享一篇對(duì)angularJs中controller控制器scope父子集作用域的實(shí)例講解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-10-10
angular2 ng build部署后base文件路徑問(wèn)題詳細(xì)解答
本篇文章主要介紹了angular2 ng build部署后base文件路徑問(wèn)題詳細(xì)解答,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-07-07

