您好,登錄后才能下訂單哦!
這篇文章主要介紹“Angular中的變化檢測實例分析”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“Angular中的變化檢測實例分析”文章能幫助大家解決問題。
變化檢測是前端框架中很有趣的一部分內容,各個前端的框架也都有自己的一套方案,一般情況下我們不太需要過多的了解變化檢測,因為框架已經幫我們完成了大部分的工作。不過隨著我們深入的使用框架,我們會發現我們很難避免的要去了解變化檢測,了解變化檢測可以幫助我們更好的理解框架、排查錯誤、進行性能優化等等。
簡單的來說,變化檢測就是通過檢測視圖與狀態之間的變化,在狀態發生了變化后,幫助我們更新視圖,這種將視圖和我們的數據同步的機制就叫變化檢測。
我們了解了什么是變化檢測,那何時觸發變化檢測呢?我們可以看看下面這兩個簡單的Demo
Demo1:
一個計數器組件,點擊按鈕Count會一直加 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:
一個Todo List的組件,通過Http獲取數據后渲染到頁面
@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; }); } }
從上面的兩個 Demo 中我們發現,在兩種情況下觸發了變化檢測:
點擊事件發生時
通過 http 請求遠程數據時
仔細思考下,這兩種觸發的方式有什么共同點呢? 我們發現這兩種方式都是異步操作,所以我們可以得出一個結論: 只要發生了異步操作,Angular 就會認為有狀態可能發生變化了,然后就會進行變化檢測。
這個時候可能大家會想到 setTimeout
setInterval
,是的,它們同樣也會觸發變化檢測。
@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; } }
簡而言之,如果發生以下事件之一,Angular 將觸發變化檢測:
任何瀏覽器事件(click、keydown 等)
setInterval()
和 setTimeout()
HTTP 通過 XMLHttpRequest
進行請求
剛才我們了解到,只要發生了異步操作,Angular 就會進行變化檢測,那 Angular 又是如何訂閱到異步事件的狀態,從而觸發變化檢測的呢?這里我們就要聊一聊 zone.js 了。
Zone.js
Zone.js 提供了一種稱為 ** 區域(Zone) ** 的機制,用于封裝和攔截瀏覽器中的異步活動、它還提供 異步生命周期的鉤子 和 統一的異步錯誤處理機制。
Zone.js 是通過 Monkey Patching(猴子補丁) 的方式來對瀏覽器中的常見方法和元素進行攔截,例如 setTimeout
和 HTMLElement.prototype.onclick
。Angular 在啟動時會利用 zone.js 修補幾個低級瀏覽器 API,從而實現異步事件的捕獲,并在捕獲時間后調用變化檢測。
下面用一段簡化的代碼來模擬一下替換 setTimeout 的過程:
function setTimeoutPatch() { // 存儲原始的setTimeout var originSetTimeout = window['setTimeout']; // 對瀏覽器原生方法的包裹封裝 window.setTimeout = function () { return global['zone']['setTimeout'].apply(global.zone, arguments); }; // 創建包裹方法,提供給上面重寫后的setTimeout使用? Zone.prototype['setTimeout'] = function (fn, delay) { // 先調用原始方法 originSetTimeout.apply(window, arguments); // 執行完原始方法后就可以做其他攔截后需要進行的操作了 ... }; }
NgZone
Zone.js 提供了一個全局區域,可以被 fork 和擴展以進一步封裝/隔離異步行為,Angular 通過創建一個fork并使用自己的行為擴展它,通常來說, 在 Angular APP 中,每個 Task 都會在 Angular 的 Zone 中運行,這個 Zone 被稱為 NgZone
。一個 Angular APP 中只存在一個 Angular Zone, 而變更檢測只會由運行于這個 ** **NgZone**
** 中的異步操作觸發 。
簡單的理解就是: Angular 通過 Zone.js 創建了一個自己的區域并稱之為 NgZone,Angular 應用中所有的異步操作都運行在這個區域中。
我們了解 Angular 的核心是 組件化 ,組件的嵌套會使得最終形成一棵 組件樹 。
Angular 在生成組件的同時,還會為每一個組件生成一個變化檢測器 changeDetector
,用來記錄組件的數據變化狀態,由于一個 Component 會對應一個 changeDetector
,所以changeDetector
同樣也是一個樹狀結構的組織。
在組件中我們可以通過注入 ChangeDetectorRef
來獲取組件的 changeDetector
@Component({ selector: "app-todos", ... }) export class TodosComponent{ constructor(cdr: ChangeDetectorRef) {} }
我們在創建一個 Angular 應用 后,Angular 會同時創建一個 ApplicationRef
的實例,這個實例代表的就是我們當前創建的這個 Angular 應用的實例。 ApplicationRef
創建的同時,會訂閱 ngZone 中的 onMicrotaskEmpty
事件,在所有的微任務完成后調用所有的視圖的detectChanges()
來執行變化檢測。
下是簡化的代碼:
class ApplicationRef { // ViewRef 是繼承于 ChangeDetectorRef 的 _views: ViewRef[] = []; constructor(private _zone: NgZone) { this._zone.onMicrotaskEmpty.subscribe({ next: () => { this._zone.run(() => { this.tick(); }); }, }); } // 執行變化檢測 tick() { for (let view of this._views) { view.detectChanges(); } } }
單向數據流
什么是單向數據流?
剛才我們說了每次觸發變化檢測,都會從根組件開始,沿著整棵組件樹從上到下的執行每個組件的變更檢測,默認情況下,直到最后一個葉子 Component 組件完成變更檢測達到穩定狀態。在這個過程中,一但父組件完成變更檢測以后,在下一次事件觸發變更檢測之前,它的子孫組件都不允許去更改父組件的變化檢測相關屬性狀態的,這就是單向數據流。
我們看一個示例:
@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 = "被修改的標題"; } }
為什么出現這個錯誤呢?
這是因為我們違反了單向數據流,ParentComponent 完成變化檢測達到穩定狀態后,ChildComponent 又改變了 ParentComponent 的數據使得 ParentComponent 需要再次被檢查,這是不被推薦的數據處理方式。在開發模式下,Angular 會進行二次檢查,如果出現上述情況,二次檢查就會報錯: ExpressionChangedAfterItHasBeenCheckedError ,在生產環境中,則只會執行一次檢查。
并不是在所有的生命周期去調用都會報錯,我們把剛才的示例修改一下:
@Component({ selector: "app-child", template: ``, }) export class ChildComponent implements OnInit { constructor(private parent: ParentComponent) {} ngOnInit(): void { this.parent.title = "被修改的標題"; } }
修改后的代碼運行正常,這是為什么呢?這里要說一下Angular檢測執行的順序:
更新所有子子組件綁定的屬性
調用所有子組件生命周期的鉤子 OnChanges, OnInit, DoCheck ,AfterContentInit
更新當前組件的DOM
調用子組件的變換檢測
調用所有子組件的生命周期鉤子 ngAfterViewInit
ngAfterViewInit
是在變化檢測之后執行的,在執行變化檢測后我們更改了父組件的數據,在Angular執行開發模式下的第二次檢查時,發現與上一次的值不一致,所以報錯,而ngOnInit
的執行在變化檢測之前,所以一切正常。
這里提一下AngularJS,AngularJS采用的是雙向數據流,錯綜復雜的數據流使得它不得不多次檢查,使得數據最終趨向穩定。理論上,數據可能永遠不穩定。AngularJS的策略是,臟檢查超過10次,就認為程序有問題,不再進行檢查。
剛才我們聊了變化檢測的工作流程,接下來我想說的是變化檢測的性能, 默認情況下,當我們的組件中某個值發生了變化觸發了變化檢測,那么Angular會從上往下檢查所有的組件。 不過Angular對每個組件進行更改檢測的速度非常快,因為它可以使用 內聯緩存 在幾毫秒內執行數千次檢查,其中內聯緩存可生成對 VM 友好代碼。
盡管 Angular 進行了大量優化,但是遇到了大型應用,變化檢測的性能仍然會下降,所以我們還需要用一些其他的方式來優化我們的應用。
Angular 提供了兩種運行變更檢測的策略:
Default
OnPush
Default 策略
默認情況下,Angular 使用 ChangeDetectionStrategy.Default
變更檢測策略,每次事件觸發變化檢測(如用戶事件、計時器、XHR、promise 等)時,此默認策略都會從上到下檢查組件樹中的每個組件。這種對組件的依賴關系不做任何假設的保守檢查方式稱為 臟檢查 ,這種策略在我們應用組件過多時會對我們的應用產生性能的影響。
OnPush 策略
Angular 還提供了一種 OnPush
策略,我們可以修改組件裝飾器的 changeDetection
來更改變化檢測的策略
@Component({ selector: 'app-demo', // 設置變化檢測的策略 changeDetection: ChangeDetectionStrategy.OnPush, template: ... }) export class DemoComponent { ... }
設置為 OnPush 策略后,Angular 每次觸發變化檢測后會跳過該組件和該組件的所以子組件變化檢測
OnPush模式下變化檢測流程
在 OnPush
策略下,只有以下這幾種情況才會觸發組件的變化檢測:
輸入值(@Input)更改
當前組件或子組件之一觸發了事件
手動觸發變化檢測
使用 async 管道后, observable 值發生了變化
在默認的變更檢測策略中,Angular 將在 @Input()
數據發生更改或修改時執行變化檢測,使用該 OnPush
時,傳入 @Input()
的值 必須是一個新的引用 才會觸發變化檢測。
JavaScript有兩種數據類型,值類型和引用類型,值類型包括:number、string、boolean、null、undefined,引用類型包括:Object、Arrary、Function,值類型每次賦值都會分配新的空間,而引用類型比如Object,直接修改屬性是引用是不會發生變化的,只有賦一個新的對象才會改變引用。
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
組件或其子組件之一觸發事件,例如 click,則將觸發變化檢測(針對組件樹中的所有組件)。
需要注意的是在 OnPush
策略中,以下操作不會觸發變化檢測:
setTimeout()
setInterval()
Promise.resolve().then()
this.http.get('...').subscribe()
有三種手動觸發更改檢測的方法:
**detectChanges(): ** 它會觸發當前組件和子組件的變化檢測
markForCheck(): 它不會觸發變化檢測,但是會把當前的OnPush組件和所以的父組件為OnPush的組件 ** 標記為需要檢測狀態** ,在當前或者下一個變化檢測周期進行檢測
ApplicationRef.tick() : 它會根據組件的變化檢測策略,觸發整個應用程序的更改檢測
可以通過 在線Demo ,更直觀的了解這幾種觸發變化檢測的方式
內置的 AsyncPipe
訂閱一個 observable 并返回它發出的最新值。
每次發出新值時的內部 AsyncPipe
調用 markForCheck
private _updateLatestValue(async: any, value: Object): void { if (async === this._obj) { this._latestValue = value; this._ref.markForCheck(); } }
剛才我們聊了變化檢測的策略,我們可以使用
OnPush
的策略來優化我們的應用,那么這就夠了嗎? 在我們實際的開發中還會有很多的場景,我們需要通過一些其他的方式來繼續優化我們的應用。
場景1:
假如我們在實現一個回車搜索的功能:
@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() { // ... } }
大家從上面的示例中可以發現什么問題呢?
我們知道事件會觸發Angular的變化檢測,在示例中綁定 keydown 事件后,每一次鍵盤輸入都會觸發變化檢測,而這些變化檢測大多數都是多余的檢測,只有當按鍵為 Enter 時,才需要真正的進行變化檢測。
在這種情況下,我們就可以利用 NgZone.runOutsideAngular()
來減少變化檢測的次數。
@Directive({ selector: '[enter]' }) export class ThyEnterDirective implements OnInit { @Output() enter = new EventEmitter(); constructor(private ngZone: NgZone, private elementRef: ElementRef<HTMLElement>) {} ngOnInit(): void { // 包裹代碼將運行在Zone區域之外 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); }); } }); }); } }
場景2:
假如我們使用 WebSocket 將大量數據從后端推送到前端,則相應的前端組件應僅每 10 秒更新一次。在這種情況下,我們可以通過調用 detach()
和手動觸發它來停用更改檢測detectChanges()
:
constructor(private cdr: ChangeDetectorRef) { cdr.detach(); // 停用變化檢測 setInterval(() => { this.cdr.detectChanges(); // 手動觸發變化檢測 }, 10 * 1000); }
當然使用 ngZone.runOutsideAngular()
也可以處理這種場景。
之前我們說了Angular 可以自動幫我們進行變化檢測,這主要是基于Zone.js來實現,那么很多人潛意識會任務Zone.js 就是 Angular 是一部分,Angular的 應用程序必須基于Zone.js,其實不然,如果我們對應用有極高的性能要求時,我們可以選擇移除 Zone.js,移除Zone.js 將會提升應用的性能和打包的體積,不過帶來的后果就是我們需要主要去調用變化檢測。
如何移除 Zone.js?
手動調用變化檢測
在 Ivy 之后,我們有一些新的API可以更方便的調用變化檢測
**?markDirty: ** 標記一個組件為 dirty 狀態 (需要重新渲染) 并將在未來某個時間點安排一個變更檢測
?detectChanges: 因為某些效率方面的原因,內部文檔不推薦使用 ?detectChanges
而推薦使用 ?markDirty
, ?detectChanges
會觸發組件以子組件的變更檢測。
移除后的性能
移除Zone.js后變化檢測由應用自己來控制,極大的減少了不必要的變化檢測次數,同時打包后的提及也減少了 36k
移除前:
移除后:
組件綁定
我們先來看一個組件綁定的例子:
按我們正常開發組件的想法,當看到這個示例的時候一定認為這個Case是Ok的,但是在運行測試后我們發現這個Case失敗了。
在生產環境中,當 Angular 創建一個組件,就會自動進行變更檢測。 但是在測試中,**TestBed.createComponent()**
并不會進行變化檢測,需要我們手動觸發。
修改一下上面的Case:
origin-url0.00KB
origin-url0.00KB
從上面的示例中可以了解到,我們必須通過調用 fixture.detectChanges()
來告訴 TestBed 執行數據綁定。
如果我們在測試中動態改變了綁定值,同樣也需要調用 fixture.detectChanges()
。
it("should update title", () => { component.title = 'Test Title'; fixture.detectChanges(); const h2 = fixture.nativeElement.querySelector("h2"); expect(h2.textContent).toContain('Test Title'); });
自動變更檢測
我們發現寫測試過程中需要頻繁的調用 fixture.detectChanges()
,可能會覺得比較繁瑣,那 Angular 可不可以在測試環境中自動運行變化檢測呢?
我們可以通過配置 ComponentFixtureAutoDetect
來實現
TestBed.configureTestingModule({ declarations: [ BannerComponent ], providers: [ { provide: ComponentFixtureAutoDetect, useValue: true } ] });
然后再回頭看看剛才的示例:
上面的示例我們并沒有調用 fixture.detectChanges()
,但是測試依然通過了,這是因為我們開啟了自動變化檢測。
再看一個示例:
上面的示例中,我們在測試代碼中動態修改了 title 的值,測試運行失敗,這是因為 Angular 并不知道測試改變了組件, ComponentFixtureAutoDetect
只對異步操作進行自動變化檢測,例如 Promise、setTimeout、click 等DOM事件等,如果我們手動更改了綁定值,我們依然還需要調用 fixture.detectChanges()
來執行變化檢測。
常見的坑
上面這個示例,綁定值修改后調用了 fixture.detectChanges()
, 但是運行測試后仍然報錯,這是為什么呢?
查看Angular源碼后我們發現 ** ngModel 的值是通過異步更新的** ,執行fixture.detectChanges()
后雖然觸發了變化檢測,但是值還并未修改成功。
修改一下測試:
修改后我們將斷言包裹在了 fixture.whenStable()
中,然后測試通過,那 whenStable()
是什么呢?
whenStable(): Promise : 當夾具穩定時解析的承諾 當事件已觸發異步活動或異步變更檢測后,可用此方法繼續執行測試。
當然除了用 fixture.whenStable()
我們也可以用 tick()
來解決這個問題
tick() :為 fakeAsync Zone 中的計時器模擬異步時間流逝 在此函數開始時以及執行任何計時器回調之后,微任務隊列就會耗盡
上面這個示例,我們在修改屬性后調用了 fixture.detectChanges()
,但是測試未通過,這是為什么呢?我們發現這個示例與第一個示例唯一的區別就是這個組件是一個 OnPush
組件,之前我們說過默認變化檢測會跳過 OnPush
組件的,只有在特定的幾種情況下才會觸發變化檢測的,遇到這種情況如何解決呢?
我們可以手動獲取組件的 ChangeDetectorRef
來主動觸發變化檢測。
虛擬DOM與增量DOM
Angular Ivy 是一個新的 Angular 渲染器,它與我們在主流框架中看到的任何東西都截然不同,因為它使用增量 DOM。 增量DOM是什么呢?它與虛擬Dom有什么不同呢?
虛擬 DOM
首先說一下虛擬DOM,我們要了解在瀏覽器中,直接操作Dom是十分損耗性能的,而虛擬DOM 的主要概念是將 UI的虛擬表示保存在內存中,通過 Diff 操作對比當前內存和上次內存中視圖的差異,從而減少不必要的Dom操作,只針對差異的Dom進行更改。
虛擬DOM執行流程:
當 UI 發生變化時,將整個 UI 渲染到 Virtual DOM 中。
計算先前和當前虛擬 DOM 表示之間的差異。
使用更改更新真實的 DOM。
虛擬 DOM 的優點:
高效的 Diff 算法。
簡單且有助于提高性能。
沒有 React 也可以使用
足夠輕量
允許構建應用程序且不考慮狀態轉換
增量Dom的主要概念是將組件編譯成一系列的指令,這些指令去創建DOM樹并在數據更改時就地的更新它們。
例如:
@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的優點:
渲染引擎可以被Tree Shakable,降低編譯后的體積
占用較低的內存
為什么可渲染引擎可以被 Tree Shakable?
Tree Shaking 是指在編譯目標代碼時移除上下文中未引用的代碼 ,增量 DOM 充分利用了這一點,因為它使用了基于指令的方法。正如示例所示,增量 DOM 在編譯之前將每個組件編譯成一組指令,這有助于識別未使用的指令。在 Tree Shakable 過程中,可以將這些未使用的的指令刪除掉。
減少內存的使用
與虛擬 DOM 不同,增量 DOM 在重新呈現應用程序 UI 時不會生成真實 DOM 的副本。此外,如果應用程序 UI 沒有變化,增量 DOM 就不會分配任何內存。大多數情況下,我們都是在沒有任何重大修改的情況下重新呈現應用程序 UI。因此,按照這種方法可以極大的減少設備內存使用。
關于“Angular中的變化檢測實例分析”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。