這篇文章主要介紹“Angular中的變化檢測(cè)實(shí)例分析”的相關(guān)知識(shí),小編通過(guò)實(shí)際案例向大家展示操作過(guò)程,操作方法簡(jiǎn)單快捷,實(shí)用性強(qiáng),希望這篇“Angular中的變化檢測(cè)實(shí)例分析”文章能幫助大家解決問(wèn)題。
成都創(chuàng)新互聯(lián)公司擁有十多年成都網(wǎng)站建設(shè)工作經(jīng)驗(yàn),為各大企業(yè)提供網(wǎng)站設(shè)計(jì)制作、網(wǎng)站設(shè)計(jì)服務(wù),對(duì)于網(wǎng)頁(yè)設(shè)計(jì)、PC網(wǎng)站建設(shè)(電腦版網(wǎng)站建設(shè))、app軟件開(kāi)發(fā)、wap網(wǎng)站建設(shè)(手機(jī)版網(wǎng)站建設(shè))、程序開(kāi)發(fā)、網(wǎng)站優(yōu)化(SEO優(yōu)化)、微網(wǎng)站、主機(jī)域名等,憑借多年來(lái)在互聯(lián)網(wǎng)的打拼,我們?cè)诨ヂ?lián)網(wǎng)網(wǎng)站建設(shè)行業(yè)積累了很多網(wǎng)站制作、網(wǎng)站設(shè)計(jì)、網(wǎng)絡(luò)營(yíng)銷經(jīng)驗(yàn),集策劃、開(kāi)發(fā)、設(shè)計(jì)、營(yíng)銷、管理等網(wǎng)站化運(yùn)作于一體,具備承接各種規(guī)模類型的網(wǎng)站建設(shè)項(xiàng)目的能力。
變化檢測(cè)是前端框架中很有趣的一部分內(nèi)容,各個(gè)前端的框架也都有自己的一套方案,一般情況下我們不太需要過(guò)多的了解變化檢測(cè),因?yàn)榭蚣芤呀?jīng)幫我們完成了大部分的工作。不過(guò)隨著我們深入的使用框架,我們會(huì)發(fā)現(xiàn)我們很難避免的要去了解變化檢測(cè),了解變化檢測(cè)可以幫助我們更好的理解框架、排查錯(cuò)誤、進(jìn)行性能優(yōu)化等等。
簡(jiǎn)單的來(lái)說(shuō),變化檢測(cè)就是通過(guò)檢測(cè)視圖與狀態(tài)之間的變化,在狀態(tài)發(fā)生了變化后,幫助我們更新視圖,這種將視圖和我們的數(shù)據(jù)同步的機(jī)制就叫變化檢測(cè)。
我們了解了什么是變化檢測(cè),那何時(shí)觸發(fā)變化檢測(cè)呢?我們可以看看下面這兩個(gè)簡(jiǎn)單的Demo
Demo1:
一個(gè)計(jì)數(shù)器組件,點(diǎn)擊按鈕Count會(huì)一直加 1
@Component({ selector: "app-counter", template: ` Count:{{ count }} <br /> <button (click)="increase()">Increase</button> `, }) export class CounterComponent { count = 0; constructor() {} increase() { this.count = this.count + 1; } }
Demo2:
一個(gè)Todo List的組件,通過(guò)Http獲取數(shù)據(jù)后渲染到頁(yè)面
@Component({ selector: "app-todos", template: ` <li *ngFor="let item of todos">{{ item.titme }}</li> `, }) export class TodosComponent implements OnInit { public todos: TodoItem[] = []; constructor(private http: HttpClient) {} ngOnInit() { this.http.get<TodoItem[]>("/api/todos").subscribe((todos: TodoItem[]) => { this.todos = todos; }); } }
從上面的兩個(gè) Demo 中我們發(fā)現(xiàn),在兩種情況下觸發(fā)了變化檢測(cè):
點(diǎn)擊事件發(fā)生時(shí)
通過(guò) http 請(qǐng)求遠(yuǎn)程數(shù)據(jù)時(shí)
仔細(xì)思考下,這兩種觸發(fā)的方式有什么共同點(diǎn)呢? 我們發(fā)現(xiàn)這兩種方式都是異步操作,所以我們可以得出一個(gè)結(jié)論: 只要發(fā)生了異步操作,Angular 就會(huì)認(rèn)為有狀態(tài)可能發(fā)生變化了,然后就會(huì)進(jìn)行變化檢測(cè)。
這個(gè)時(shí)候可能大家會(huì)想到 setTimeout
setInterval
,是的,它們同樣也會(huì)觸發(fā)變化檢測(cè)。
@Component({ selector: "app-counter", template: ` Count:{{ count }} <br /> <button (click)="increase()">Increase</button> `, }) export class CounterComponent implements OnInit { count = 0; constructor() {} ngOnInit(){ setTimeout(()=>{ this.count= 10; }); } increase() { this.count = this.count + 1; } }
簡(jiǎn)而言之,如果發(fā)生以下事件之一,Angular 將觸發(fā)變化檢測(cè):
任何瀏覽器事件(click、keydown 等)
setInterval()
和 setTimeout()
HTTP 通過(guò) XMLHttpRequest
進(jìn)行請(qǐng)求
剛才我們了解到,只要發(fā)生了異步操作,Angular 就會(huì)進(jìn)行變化檢測(cè),那 Angular 又是如何訂閱到異步事件的狀態(tài),從而觸發(fā)變化檢測(cè)的呢?這里我們就要聊一聊 zone.js 了。
Zone.js
Zone.js 提供了一種稱為 ** 區(qū)域(Zone) ** 的機(jī)制,用于封裝和攔截瀏覽器中的異步活動(dòng)、它還提供 異步生命周期的鉤子和 統(tǒng)一的異步錯(cuò)誤處理機(jī)制。
Zone.js 是通過(guò) Monkey Patching(猴子補(bǔ)?。?/strong>的方式來(lái)對(duì)瀏覽器中的常見(jiàn)方法和元素進(jìn)行攔截,例如 setTimeout
和 HTMLElement.prototype.onclick
。Angular 在啟動(dòng)時(shí)會(huì)利用 zone.js 修補(bǔ)幾個(gè)低級(jí)瀏覽器 API,從而實(shí)現(xiàn)異步事件的捕獲,并在捕獲時(shí)間后調(diào)用變化檢測(cè)。
下面用一段簡(jiǎn)化的代碼來(lái)模擬一下替換 setTimeout 的過(guò)程:
function setTimeoutPatch() { // 存儲(chǔ)原始的setTimeout var originSetTimeout = window['setTimeout']; // 對(duì)瀏覽器原生方法的包裹封裝 window.setTimeout = function () { return global['zone']['setTimeout'].apply(global.zone, arguments); }; // 創(chuàng)建包裹方法,提供給上面重寫(xiě)后的setTimeout使用? Zone.prototype['setTimeout'] = function (fn, delay) { // 先調(diào)用原始方法 originSetTimeout.apply(window, arguments); // 執(zhí)行完原始方法后就可以做其他攔截后需要進(jìn)行的操作了 ... }; }
NgZone
Zone.js 提供了一個(gè)全局區(qū)域,可以被 fork 和擴(kuò)展以進(jìn)一步封裝/隔離異步行為,Angular 通過(guò)創(chuàng)建一個(gè)fork并使用自己的行為擴(kuò)展它,通常來(lái)說(shuō), 在 Angular APP 中,每個(gè) Task 都會(huì)在 Angular 的 Zone 中運(yùn)行,這個(gè) Zone 被稱為 NgZone
。一個(gè) Angular APP 中只存在一個(gè) Angular Zone, 而變更檢測(cè)只會(huì)由運(yùn)行于這個(gè) ** **NgZone**
** 中的異步操作觸發(fā)。
簡(jiǎn)單的理解就是: Angular 通過(guò) Zone.js 創(chuàng)建了一個(gè)自己的區(qū)域并稱之為 NgZone,Angular 應(yīng)用中所有的異步操作都運(yùn)行在這個(gè)區(qū)域中。
我們了解 Angular 的核心是 組件化,組件的嵌套會(huì)使得最終形成一棵 組件樹(shù)。
Angular 在生成組件的同時(shí),還會(huì)為每一個(gè)組件生成一個(gè)變化檢測(cè)器 changeDetector
,用來(lái)記錄組件的數(shù)據(jù)變化狀態(tài),由于一個(gè) Component 會(huì)對(duì)應(yīng)一個(gè) changeDetector
,所以changeDetector
同樣也是一個(gè)樹(shù)狀結(jié)構(gòu)的組織。
在組件中我們可以通過(guò)注入 ChangeDetectorRef
來(lái)獲取組件的 changeDetector
@Component({ selector: "app-todos", ... }) export class TodosComponent{ constructor(cdr: ChangeDetectorRef) {} }
我們?cè)趧?chuàng)建一個(gè) Angular 應(yīng)用 后,Angular 會(huì)同時(shí)創(chuàng)建一個(gè) ApplicationRef
的實(shí)例,這個(gè)實(shí)例代表的就是我們當(dāng)前創(chuàng)建的這個(gè) Angular 應(yīng)用的實(shí)例。 ApplicationRef
創(chuàng)建的同時(shí),會(huì)訂閱 ngZone 中的 onMicrotaskEmpty
事件,在所有的微任務(wù)完成后調(diào)用所有的視圖的detectChanges()
來(lái)執(zhí)行變化檢測(cè)。
下是簡(jiǎn)化的代碼:
class ApplicationRef { // ViewRef 是繼承于 ChangeDetectorRef 的 _views: ViewRef[] = []; constructor(private _zone: NgZone) { this._zone.onMicrotaskEmpty.subscribe({ next: () => { this._zone.run(() => { this.tick(); }); }, }); } // 執(zhí)行變化檢測(cè) tick() { for (let view of this._views) { view.detectChanges(); } } }
單向數(shù)據(jù)流
什么是單向數(shù)據(jù)流?
剛才我們說(shuō)了每次觸發(fā)變化檢測(cè),都會(huì)從根組件開(kāi)始,沿著整棵組件樹(shù)從上到下的執(zhí)行每個(gè)組件的變更檢測(cè),默認(rèn)情況下,直到最后一個(gè)葉子 Component 組件完成變更檢測(cè)達(dá)到穩(wěn)定狀態(tài)。在這個(gè)過(guò)程中,一但父組件完成變更檢測(cè)以后,在下一次事件觸發(fā)變更檢測(cè)之前,它的子孫組件都不允許去更改父組件的變化檢測(cè)相關(guān)屬性狀態(tài)的,這就是單向數(shù)據(jù)流。
我們看一個(gè)示例:
@Component({ selector: "app-parent", template: ` {{ title }} <app-child></app-child> `, }) export class ParentComponent { title = "我的父組件"; } @Component({ selector: "app-child", template: ``, }) export class ChildComponent implements AfterViewInit { constructor(private parent: ParentComponent) {} ngAfterViewInit(): void { this.parent.title = "被修改的標(biāo)題"; } }
為什么出現(xiàn)這個(gè)錯(cuò)誤呢?
這是因?yàn)槲覀冞`反了單向數(shù)據(jù)流,ParentComponent 完成變化檢測(cè)達(dá)到穩(wěn)定狀態(tài)后,ChildComponent 又改變了 ParentComponent 的數(shù)據(jù)使得 ParentComponent 需要再次被檢查,這是不被推薦的數(shù)據(jù)處理方式。在開(kāi)發(fā)模式下,Angular 會(huì)進(jìn)行二次檢查,如果出現(xiàn)上述情況,二次檢查就會(huì)報(bào)錯(cuò): ExpressionChangedAfterItHasBeenCheckedError ,在生產(chǎn)環(huán)境中,則只會(huì)執(zhí)行一次檢查。
并不是在所有的生命周期去調(diào)用都會(huì)報(bào)錯(cuò),我們把剛才的示例修改一下:
@Component({ selector: "app-child", template: ``, }) export class ChildComponent implements OnInit { constructor(private parent: ParentComponent) {} ngOnInit(): void { this.parent.title = "被修改的標(biāo)題"; } }
修改后的代碼運(yùn)行正常,這是為什么呢?這里要說(shuō)一下Angular檢測(cè)執(zhí)行的順序:
更新所有子子組件綁定的屬性
調(diào)用所有子組件生命周期的鉤子 OnChanges, OnInit, DoCheck ,AfterContentInit
更新當(dāng)前組件的DOM
調(diào)用子組件的變換檢測(cè)
調(diào)用所有子組件的生命周期鉤子 ngAfterViewInit
ngAfterViewInit
是在變化檢測(cè)之后執(zhí)行的,在執(zhí)行變化檢測(cè)后我們更改了父組件的數(shù)據(jù),在Angular執(zhí)行開(kāi)發(fā)模式下的第二次檢查時(shí),發(fā)現(xiàn)與上一次的值不一致,所以報(bào)錯(cuò),而ngOnInit
的執(zhí)行在變化檢測(cè)之前,所以一切正常。
這里提一下AngularJS,AngularJS采用的是雙向數(shù)據(jù)流,錯(cuò)綜復(fù)雜的數(shù)據(jù)流使得它不得不多次檢查,使得數(shù)據(jù)最終趨向穩(wěn)定。理論上,數(shù)據(jù)可能永遠(yuǎn)不穩(wěn)定。AngularJS的策略是,臟檢查超過(guò)10次,就認(rèn)為程序有問(wèn)題,不再進(jìn)行檢查。
剛才我們聊了變化檢測(cè)的工作流程,接下來(lái)我想說(shuō)的是變化檢測(cè)的性能, 默認(rèn)情況下,當(dāng)我們的組件中某個(gè)值發(fā)生了變化觸發(fā)了變化檢測(cè),那么Angular會(huì)從上往下檢查所有的組件。不過(guò)Angular對(duì)每個(gè)組件進(jìn)行更改檢測(cè)的速度非???,因?yàn)樗梢允褂?內(nèi)聯(lián)緩存 在幾毫秒內(nèi)執(zhí)行數(shù)千次檢查,其中內(nèi)聯(lián)緩存可生成對(duì) VM 友好代碼。
盡管 Angular 進(jìn)行了大量?jī)?yōu)化,但是遇到了大型應(yīng)用,變化檢測(cè)的性能仍然會(huì)下降,所以我們還需要用一些其他的方式來(lái)優(yōu)化我們的應(yīng)用。
Angular 提供了兩種運(yùn)行變更檢測(cè)的策略:
Default
OnPush
Default 策略
默認(rèn)情況下,Angular 使用 ChangeDetectionStrategy.Default
變更檢測(cè)策略,每次事件觸發(fā)變化檢測(cè)(如用戶事件、計(jì)時(shí)器、XHR、promise 等)時(shí),此默認(rèn)策略都會(huì)從上到下檢查組件樹(shù)中的每個(gè)組件。這種對(duì)組件的依賴關(guān)系不做任何假設(shè)的保守檢查方式稱為 臟檢查,這種策略在我們應(yīng)用組件過(guò)多時(shí)會(huì)對(duì)我們的應(yīng)用產(chǎn)生性能的影響。
OnPush 策略
Angular 還提供了一種 OnPush
策略,我們可以修改組件裝飾器的 changeDetection
來(lái)更改變化檢測(cè)的策略
@Component({ selector: 'app-demo', // 設(shè)置變化檢測(cè)的策略 changeDetection: ChangeDetectionStrategy.OnPush, template: ... }) export class DemoComponent { ... }
設(shè)置為 OnPush 策略后,Angular 每次觸發(fā)變化檢測(cè)后會(huì)跳過(guò)該組件和該組件的所以子組件變化檢測(cè)
OnPush模式下變化檢測(cè)流程
在 OnPush
策略下,只有以下這幾種情況才會(huì)觸發(fā)組件的變化檢測(cè):
輸入值(@Input)更改
當(dāng)前組件或子組件之一觸發(fā)了事件
手動(dòng)觸發(fā)變化檢測(cè)
使用 async 管道后, observable 值發(fā)生了變化
在默認(rèn)的變更檢測(cè)策略中,Angular 將在 @Input()
數(shù)據(jù)發(fā)生更改或修改時(shí)執(zhí)行變化檢測(cè),使用該 OnPush
時(shí),傳入 @Input()
的值 必須是一個(gè)新的引用才會(huì)觸發(fā)變化檢測(cè)。
JavaScript有兩種數(shù)據(jù)類型,值類型和引用類型,值類型包括:number、string、boolean、null、undefined,引用類型包括:Object、Arrary、Function,值類型每次賦值都會(huì)分配新的空間,而引用類型比如Object,直接修改屬性是引用是不會(huì)發(fā)生變化的,只有賦一個(gè)新的對(duì)象才會(huì)改變引用。
var a= 1; var b = a; b = 2; console.log(a==b); // false var obj1 = {a:1}; var obj2 = obj1; obj2.a = 2; console.log(obj1); // {a:2} console.log(obj1 === obj2); //true obj2= {...obj1}; console.log(obj1 === obj2); //false
如果 OnPush
組件或其子組件之一觸發(fā)事件,例如 click,則將觸發(fā)變化檢測(cè)(針對(duì)組件樹(shù)中的所有組件)。
需要注意的是在 OnPush
策略中,以下操作不會(huì)觸發(fā)變化檢測(cè):
setTimeout()
setInterval()
Promise.resolve().then()
this.http.get('...').subscribe()
有三種手動(dòng)觸發(fā)更改檢測(cè)的方法:
**detectChanges(): ** 它會(huì)觸發(fā)當(dāng)前組件和子組件的變化檢測(cè)
markForCheck():它不會(huì)觸發(fā)變化檢測(cè),但是會(huì)把當(dāng)前的OnPush組件和所以的父組件為OnPush的組件 ** 標(biāo)記為需要檢測(cè)狀態(tài)** ,在當(dāng)前或者下一個(gè)變化檢測(cè)周期進(jìn)行檢測(cè)
ApplicationRef.tick(): 它會(huì)根據(jù)組件的變化檢測(cè)策略,觸發(fā)整個(gè)應(yīng)用程序的更改檢測(cè)
可以通過(guò) 在線Demo ,更直觀的了解這幾種觸發(fā)變化檢測(cè)的方式
內(nèi)置的 AsyncPipe
訂閱一個(gè) observable 并返回它發(fā)出的最新值。
每次發(fā)出新值時(shí)的內(nèi)部 AsyncPipe
調(diào)用 markForCheck
private _updateLatestValue(async: any, value: Object): void { if (async === this._obj) { this._latestValue = value; this._ref.markForCheck(); } }
剛才我們聊了變化檢測(cè)的策略,我們可以使用
OnPush
的策略來(lái)優(yōu)化我們的應(yīng)用,那么這就夠了嗎? 在我們實(shí)際的開(kāi)發(fā)中還會(huì)有很多的場(chǎng)景,我們需要通過(guò)一些其他的方式來(lái)繼續(xù)優(yōu)化我們的應(yīng)用。
場(chǎng)景1:
假如我們?cè)趯?shí)現(xiàn)一個(gè)回車搜索的功能:
@Component({ selector: "app-enter", template: `<input #input type="text" />`, }) export class EnterComponent implements AfterViewInit { @ViewChild("input", { read: ElementRef }) private inputElementRef: any; constructor() {} ngAfterViewInit(): void { this.inputElementRef.nativeElement.addEventListener( "keydown", (event: KeyboardEvent) => { const keyCode = event.which || event.keyCode; if (keyCode === 13) { this.search(); } } ); } search() { // ... } }
大家從上面的示例中可以發(fā)現(xiàn)什么問(wèn)題呢?
我們知道事件會(huì)觸發(fā)Angular的變化檢測(cè),在示例中綁定 keydown 事件后,每一次鍵盤(pán)輸入都會(huì)觸發(fā)變化檢測(cè),而這些變化檢測(cè)大多數(shù)都是多余的檢測(cè),只有當(dāng)按鍵為 Enter 時(shí),才需要真正的進(jìn)行變化檢測(cè)。
在這種情況下,我們就可以利用 NgZone.runOutsideAngular()
來(lái)減少變化檢測(cè)的次數(shù)。
@Directive({ selector: '[enter]' }) export class ThyEnterDirective implements OnInit { @Output() enter = new EventEmitter(); constructor(private ngZone: NgZone, private elementRef: ElementRef<HTMLElement>) {} ngOnInit(): void { // 包裹代碼將運(yùn)行在Zone區(qū)域之外 this.ngZone.runOutsideAngular(() => { this.elementRef.nativeElement.addEventListener('keydown', (event: KeyboardEvent) => { const keyCode = event.which || event.keyCode; if (keyCode === 13) { this.ngZone.run(() => { this.enter.emit(event); }); } }); }); } }
場(chǎng)景2:
假如我們使用 WebSocket 將大量數(shù)據(jù)從后端推送到前端,則相應(yīng)的前端組件應(yīng)僅每 10 秒更新一次。在這種情況下,我們可以通過(guò)調(diào)用 detach()
和手動(dòng)觸發(fā)它來(lái)停用更改檢測(cè)detectChanges()
:
constructor(private cdr: ChangeDetectorRef) { cdr.detach(); // 停用變化檢測(cè) setInterval(() => { this.cdr.detectChanges(); // 手動(dòng)觸發(fā)變化檢測(cè) }, 10 * 1000); }
當(dāng)然使用 ngZone.runOutsideAngular()
也可以處理這種場(chǎng)景。
之前我們說(shuō)了Angular 可以自動(dòng)幫我們進(jìn)行變化檢測(cè),這主要是基于Zone.js來(lái)實(shí)現(xiàn),那么很多人潛意識(shí)會(huì)任務(wù)Zone.js 就是 Angular 是一部分,Angular的 應(yīng)用程序必須基于Zone.js,其實(shí)不然,如果我們對(duì)應(yīng)用有極高的性能要求時(shí),我們可以選擇移除 Zone.js,移除Zone.js 將會(huì)提升應(yīng)用的性能和打包的體積,不過(guò)帶來(lái)的后果就是我們需要主要去調(diào)用變化檢測(cè)。
如何移除 Zone.js?
手動(dòng)調(diào)用變化檢測(cè)
在 Ivy 之后,我們有一些新的API可以更方便的調(diào)用變化檢測(cè)
**?markDirty: ** 標(biāo)記一個(gè)組件為 dirty 狀態(tài) (需要重新渲染) 并將在未來(lái)某個(gè)時(shí)間點(diǎn)安排一個(gè)變更檢測(cè)
?detectChanges:因?yàn)槟承┬史矫娴脑?,?nèi)部文檔不推薦使用 ?detectChanges
而推薦使用 ?markDirty
, ?detectChanges
會(huì)觸發(fā)組件以子組件的變更檢測(cè)。
移除后的性能
移除Zone.js后變化檢測(cè)由應(yīng)用自己來(lái)控制,極大的減少了不必要的變化檢測(cè)次數(shù),同時(shí)打包后的提及也減少了 36k
移除前:
移除后:
組件綁定
我們先來(lái)看一個(gè)組件綁定的例子:
按我們正常開(kāi)發(fā)組件的想法,當(dāng)看到這個(gè)示例的時(shí)候一定認(rèn)為這個(gè)Case是Ok的,但是在運(yùn)行測(cè)試后我們發(fā)現(xiàn)這個(gè)Case失敗了。
在生產(chǎn)環(huán)境中,當(dāng) Angular 創(chuàng)建一個(gè)組件,就會(huì)自動(dòng)進(jìn)行變更檢測(cè)。 但是在測(cè)試中,**TestBed.createComponent()**
并不會(huì)進(jìn)行變化檢測(cè),需要我們手動(dòng)觸發(fā)。
修改一下上面的Case:
origin-url0.00KB
origin-url0.00KB
從上面的示例中可以了解到,我們必須通過(guò)調(diào)用 fixture.detectChanges()
來(lái)告訴 TestBed 執(zhí)行數(shù)據(jù)綁定。
如果我們?cè)跍y(cè)試中動(dòng)態(tài)改變了綁定值,同樣也需要調(diào)用 fixture.detectChanges()
。
it("should update title", () => { component.title = 'Test Title'; fixture.detectChanges(); const h2 = fixture.nativeElement.querySelector("h2"); expect(h2.textContent).toContain('Test Title'); });
自動(dòng)變更檢測(cè)
我們發(fā)現(xiàn)寫(xiě)測(cè)試過(guò)程中需要頻繁的調(diào)用 fixture.detectChanges()
,可能會(huì)覺(jué)得比較繁瑣,那 Angular 可不可以在測(cè)試環(huán)境中自動(dòng)運(yùn)行變化檢測(cè)呢?
我們可以通過(guò)配置 ComponentFixtureAutoDetect
來(lái)實(shí)現(xiàn)
TestBed.configureTestingModule({ declarations: [ BannerComponent ], providers: [ { provide: ComponentFixtureAutoDetect, useValue: true } ] });
然后再回頭看看剛才的示例:
上面的示例我們并沒(méi)有調(diào)用 fixture.detectChanges()
,但是測(cè)試依然通過(guò)了,這是因?yàn)槲覀冮_(kāi)啟了自動(dòng)變化檢測(cè)。
再看一個(gè)示例:
上面的示例中,我們?cè)跍y(cè)試代碼中動(dòng)態(tài)修改了 title 的值,測(cè)試運(yùn)行失敗,這是因?yàn)?Angular 并不知道測(cè)試改變了組件, ComponentFixtureAutoDetect
只對(duì)異步操作進(jìn)行自動(dòng)變化檢測(cè),例如 Promise、setTimeout、click 等DOM事件等,如果我們手動(dòng)更改了綁定值,我們依然還需要調(diào)用 fixture.detectChanges()
來(lái)執(zhí)行變化檢測(cè)。
常見(jiàn)的坑
上面這個(gè)示例,綁定值修改后調(diào)用了 fixture.detectChanges()
, 但是運(yùn)行測(cè)試后仍然報(bào)錯(cuò),這是為什么呢?
查看Angular源碼后我們發(fā)現(xiàn) ** ngModel 的值是通過(guò)異步更新的** ,執(zhí)行fixture.detectChanges()
后雖然觸發(fā)了變化檢測(cè),但是值還并未修改成功。
修改一下測(cè)試:
修改后我們將斷言包裹在了 fixture.whenStable()
中,然后測(cè)試通過(guò),那 whenStable()
是什么呢?
whenStable(): Promise : 當(dāng)夾具穩(wěn)定時(shí)解析的承諾 當(dāng)事件已觸發(fā)異步活動(dòng)或異步變更檢測(cè)后,可用此方法繼續(xù)執(zhí)行測(cè)試。
當(dāng)然除了用 fixture.whenStable()
我們也可以用 tick()
來(lái)解決這個(gè)問(wèn)題
tick() :為 fakeAsync Zone 中的計(jì)時(shí)器模擬異步時(shí)間流逝 在此函數(shù)開(kāi)始時(shí)以及執(zhí)行任何計(jì)時(shí)器回調(diào)之后,微任務(wù)隊(duì)列就會(huì)耗盡
上面這個(gè)示例,我們?cè)谛薷膶傩院笳{(diào)用了 fixture.detectChanges()
,但是測(cè)試未通過(guò),這是為什么呢?我們發(fā)現(xiàn)這個(gè)示例與第一個(gè)示例唯一的區(qū)別就是這個(gè)組件是一個(gè) OnPush
組件,之前我們說(shuō)過(guò)默認(rèn)變化檢測(cè)會(huì)跳過(guò) OnPush
組件的,只有在特定的幾種情況下才會(huì)觸發(fā)變化檢測(cè)的,遇到這種情況如何解決呢?
我們可以手動(dòng)獲取組件的 ChangeDetectorRef
來(lái)主動(dòng)觸發(fā)變化檢測(cè)。
虛擬DOM與增量DOM
Angular Ivy 是一個(gè)新的 Angular 渲染器,它與我們?cè)谥髁骺蚣苤锌吹降娜魏螙|西都截然不同,因?yàn)樗褂迷隽?DOM。 增量DOM是什么呢?它與虛擬Dom有什么不同呢?
虛擬 DOM
首先說(shuō)一下虛擬DOM,我們要了解在瀏覽器中,直接操作Dom是十分損耗性能的,而虛擬DOM 的主要概念是將 UI的虛擬表示保存在內(nèi)存中,通過(guò) Diff 操作對(duì)比當(dāng)前內(nèi)存和上次內(nèi)存中視圖的差異,從而減少不必要的Dom操作,只針對(duì)差異的Dom進(jìn)行更改。
虛擬DOM執(zhí)行流程:
當(dāng) UI 發(fā)生變化時(shí),將整個(gè) UI 渲染到 Virtual DOM 中。
計(jì)算先前和當(dāng)前虛擬 DOM 表示之間的差異。
使用更改更新真實(shí)的 DOM。
虛擬 DOM 的優(yōu)點(diǎn):
高效的 Diff 算法。
簡(jiǎn)單且有助于提高性能。
沒(méi)有 React 也可以使用
足夠輕量
允許構(gòu)建應(yīng)用程序且不考慮狀態(tài)轉(zhuǎn)換
增量Dom的主要概念是將組件編譯成一系列的指令,這些指令去創(chuàng)建DOM樹(shù)并在數(shù)據(jù)更改時(shí)就地的更新它們。
例如:
@Component({ selector: 'todos-cmp', template: ` <p *ngFor="let t of todos|async"> {{t.description}} </p> ` }) class TodosComponent { todos: Observable<Todo[]> = this.store.pipe(select('todos')); constructor(private store: Store<AppState>) {} }
編譯后:
var TodosComponent = /** @class */ (function () { function TodosComponent(store) { this.store = store; this.todos = this.store.pipe(select('todos')); } TodosComponent.ngComponentDef = defineComponent({ type: TodosComponent, selectors: [["todos-cmp"]], factory: function TodosComponent_Factory(t) { return new (t || TodosComponent)(directiveInject(Store)); }, consts: 2, vars: 3, template: function TodosComponent_Template(rf, ctx) { if (rf & 1) { // create dom pipe(1, "async"); template(0, TodosComponent_p_Template_0, 2, 1, null, _c0); } if (rf & 2) { // update dom elementProperty(0, "ngForOf", bind(pipeBind1(1, 1, ctx.todos))); } }, encapsulation: 2 }); return TodosComponent; }());
增量DOM的優(yōu)點(diǎn):
渲染引擎可以被Tree Shakable,降低編譯后的體積
占用較低的內(nèi)存
為什么可渲染引擎可以被 Tree Shakable?
Tree Shaking 是指在編譯目標(biāo)代碼時(shí)移除上下文中未引用的代碼,增量 DOM 充分利用了這一點(diǎn),因?yàn)樗褂昧嘶谥噶畹姆椒āU缡纠?,增?DOM 在編譯之前將每個(gè)組件編譯成一組指令,這有助于識(shí)別未使用的指令。在 Tree Shakable 過(guò)程中,可以將這些未使用的的指令刪除掉。
減少內(nèi)存的使用
與虛擬 DOM 不同,增量 DOM 在重新呈現(xiàn)應(yīng)用程序 UI 時(shí)不會(huì)生成真實(shí) DOM 的副本。此外,如果應(yīng)用程序 UI 沒(méi)有變化,增量 DOM 就不會(huì)分配任何內(nèi)存。大多數(shù)情況下,我們都是在沒(méi)有任何重大修改的情況下重新呈現(xiàn)應(yīng)用程序 UI。因此,按照這種方法可以極大的減少設(shè)備內(nèi)存使用。
關(guān)于“Angular中的變化檢測(cè)實(shí)例分析”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí),可以關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,小編每天都會(huì)為大家更新不同的知識(shí)點(diǎn)。
文章題目:Angular中的變化檢測(cè)實(shí)例分析
網(wǎng)頁(yè)URL:http://jinyejixie.com/article28/gcspjp.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供、網(wǎng)站收錄、網(wǎng)頁(yè)設(shè)計(jì)公司、定制開(kāi)發(fā)、關(guān)鍵詞優(yōu)化、移動(dòng)網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)