變更偵測是Angular中很重要的一部分,也就是模型和視圖之間保持同步。在日常開發過程中,我們無需了解變更偵測,因為Angular都幫我們完成了這一部分工作,讓開發人員更專注於業務實現,提高開發效率和開發體驗。但是如果想要深入使用框架,或者想要寫出高效能的程式碼而不只是實現了功能,就必須要去了解變更檢測,它可以幫助我們更好的理解框架,調試錯誤,提高性能等。 【相關教學推薦:《angular教學》】
我們先來看一個小範例。
當我們點選按鈕的時候,就改變了name屬性,同時DOM自動被更新成新的name值。
那現在有一個問題,如果我改變name的值後,就緊接著把DOM中的innerText輸出出來,它會是什麼值呢?
import { Component, ViewChild, ElementRef } from '@angular/core'; @Component({ selector: 'my-app', templateUrl: './app.component.html', styleUrls: [ './app.component.css' ] }) export class AppComponent { name = 'Empty'; @ViewChild('textContainer') textContainer: ElementRef; normalClick(): void { this.name = 'Hello Angular'; console.log(this.textContainer.nativeElement.innerText); } }
你答對了嗎?
那這兩段程式碼到底發生了什麼事?
如果我們用原生JS來寫這段程式碼,那麼點擊按鈕後的視圖肯定不會發生任何變化,而在Angular中卻讓視圖發生了變化,那它為什麼會自動把視圖更新了呢?這離不開一個叫做zone.js的函式庫,簡單來說,它是對發生值改變的事件做了一些處理,這個會在後面的部分詳細講解,這裡暫時知道這個就可以了。
如果我不想讓這個函式庫做這些處理,Angular也為我們提供了禁用zone.js的方法。
可以在main.ts中設定禁用zone.js。
import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' }) .catch(err => console.error(err));
當我們停用zone.js,視圖並未發生更新。到原始碼找一下視圖更新的相關程式碼。
*/ class ApplicationRef { /** @internal */ constructor(_zone, _injector, _exceptionHandler, _initStatus) { this._zone = _zone; this._injector = _injector; this._exceptionHandler = _exceptionHandler; this._initStatus = _initStatus; /** @internal */ this._bootstrapListeners = []; this._views = []; this._runningTick = false; this._stable = true; this._destroyed = false; this._destroyListeners = []; /** * Get a list of component types registered to this application. * This list is populated even before the component is created. */ this.componentTypes = []; /** * Get a list of components registered to this application. */ this.components = []; this._onMicrotaskEmptySubscription = this._zone.onMicrotaskEmpty.subscribe({ next: () => { this._zone.run(() => { this.tick(); }); } }); ... } /** * Invoke this method to explicitly process change detection and its side-effects. * * In development mode, `tick()` also performs a second change detection cycle to ensure that no * further changes are detected. If additional changes are picked up during this second cycle, * bindings in the app have side-effects that cannot be resolved in a single change detection * pass. * In this case, Angular throws an error, since an Angular application can only have one change * detection pass during which all change detection must complete. */ tick() { NG_DEV_MODE && this.warnIfDestroyed(); if (this._runningTick) { const errorMessage = (typeof ngDevMode === 'undefined' || ngDevMode) ? 'ApplicationRef.tick is called recursively' : ''; throw new RuntimeError(101 /* RuntimeErrorCode.RECURSIVE_APPLICATION_REF_TICK */, errorMessage); } try { this._runningTick = true; for (let view of this._views) { view.detectChanges(); } if (typeof ngDevMode === 'undefined' || ngDevMode) { for (let view of this._views) { view.checkNoChanges(); } } } catch (e) { // Attention: Don't rethrow as it could cancel subscriptions to Observables! this._zone.runOutsideAngular(() => this._exceptionHandler.handleError(e)); } finally { this._runningTick = false; } } }
大致解讀一下,這個ApplicationRef是Angular整個應用程式的實例,在建構函式中,zone(zone函式庫)的onMicrotaskEmpty (從名字上看是一個清空微任務的一個subject)訂閱了一下。在訂閱裡,呼叫了tick(),那tick裡做了什麼呢?
思考: 上次說了最好訂閱不要放到constructor裡去訂閱,這裡怎麼這麼不規範呢?
當然不是,上次我們說的是Angular組件裡哪些應該放constructor,哪些應該放ngOnInit裡的情況。但這裡,ApplicationRef人家是一個service呀,只能將初始化的程式碼放constructor。
在tick函數裡,如果發現這個tick函數正在執行,則會拋出異常,因為這個是整個應用的實例,不能遞歸呼叫。然後,遍歷了所有個views,然後每個view都執行了detectChanges(),也就是執行了下變更偵測,什麼是變更偵測,會在後面詳細講解。緊接著,如果是devMode,再次遍歷所有的views,每個view執行了checkNoChanges(),檢查一下有沒有變化,有變化則會拋錯(後面會詳細說這個問題,暫時跳過)。
那好了,現在也知道怎麼能讓它更新了,就是要呼叫一下ApplicationRef的tick方法。
import { Component, ViewChild, ElementRef, ApplicationRef } from '@angular/core'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent { name = 'Empty'; @ViewChild('textContainer') textContainer: ElementRef = {} as any; constructor(private app: ApplicationRef){} normalClick(): void { this.name = 'Hello Angular'; console.log(this.textContainer.nativeElement.innerText); this.app.tick(); } }
果然,可以正常的更新視圖了。
我們來簡單梳理一下,DOM的更新依賴於tick() 的觸發,zone.js幫助開發者無需手動觸發這個操作。好了,現在可以把zone.js啟用了。
那什麼是變更偵測呢?繼續期待下一篇。
更多程式相關知識,請造訪:程式設計教學! !
以上是淺析Angular變更偵測中的DOM更新機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!