您好,登錄后才能下訂單哦!
小編給大家分享一下Angular中如何實現變更檢測,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!
我們先來看一個最簡單地 demo
在按鈕被點擊時,我們改變了組件的 name 屬性,就在一瞬間 DOM 中也顯示出了改變后的值,這似乎有些“神奇”。
如果緊跟著元素更改的語句之后打印出真實 DOM 中的 innterText,卻發現仍然還是舊的值,可是明明視圖中的值已經改變了,這兩段代碼中到底發生了什么呢?如果你也對此也疑惑不已,那么就和我一起來揭曉這個答案。
我們仔細回憶下剛剛發生的事情:
點擊按鈕
值改變
如果使用原生 JS 來編寫這段代碼,那么點擊按鈕后的視圖肯定不會發生改變,而在 Angular 中卻讓視圖發生了改變。如果你對 Angular 有稍微深入的了解,就會知道一個叫做 zone.js 的庫,仔細翻看就會發現,zone.js 對所有可能發生值改變的事件做了一層處理,比如:
Events:click,mouseover,mouseout,keyup,keydown 等所有的瀏覽器事件
Timer:setTimeout,setInterval
XHR:各類請求等
Angular 還為我們提供了禁用 zone.js 的方法。
禁用 zone 后,當我們再次點擊按鈕時,視圖未更新。
帶著好奇心,我們找到 Angular 源碼中視圖更新的關鍵代碼
這一次我們手動在代碼調用這個方法。
果然和預料中的一樣!視圖更新了,更讓人驚喜是,打印出來的 innerText 也更新了!
到這里,我們得出了一個結論,DOM 的更新依賴于 tick() 的觸發,zone.js 幫助開發者省去了手動觸發的操作。
好了,小試牛刀之后,接下里我們就來仔細探究 Angular 視圖更新的背后到底發生了什么。
我們先來看這樣一處錯誤,在 child 組件的 ngOnInit 中更改了父組件 parent 的 name 值,結果出現了大家一定都遇到過的錯誤信息
可是這樣寫并不是每次都會報錯,例如我們去掉子組件 child 的輸入屬性,刷新一下,發現同樣的代碼卻可以運行,父組件的 name 可以被正常更改。
emmm... 陷入沉思...
也許你和剛開始學習 Angular 時的我一樣,在 stackoverflow 里搜索這個問題,復制了個自己也不知道為什么能起作用的代碼就直接粘貼了上去,后面再遇到這個問題時,繼續在 stackoverflow 里搜索和復制粘貼,如此反復...
隨著時間的推移,精通各種 CRUD 的你越來越不滿足于這種面向 stackoverflow 編程的自己,開始在社區、文檔、論壇上不停的查找問題的答案,但是看完他們的回答和文章,好像只知道了有個叫做變更檢測的東西,但是具體是怎么導致了這個 bug ,卻支支吾吾的說不太清楚,如果你也和我一樣對上述經歷深有體會,那么就繼續向下探尋真相吧!
當我們在 model 中改變數據時,框架層需要知道:
model 哪里發生了改變
view 中哪里需要更新
React 中的 Virtual Dom 大家一定都不陌生,React 通過對比 DOM 的新狀態與舊狀態來決定更新哪一部分 dom,而不是更新所有的 dom,這也是 Angular 中變更檢測(change detection)的異曲同工之處。
整個 Angular 應用是個組件樹,不可能任意一個組件中的改變都觸發所有組件的更新,這樣效率太低也太耗時,例如用戶更改了某個 button 的狀態,那么最理想的做法是只更新這個 button 的樣式或文字,而不是整個應用全部更新一遍,變更檢測的目的也就是為此。
默認情況下(ChangeDetectionStrategy.Default
),父組件的變更檢測發生時,子組件也會觸發變更檢測。
(CD
即為 changeDetection
)
每次變更檢測時,都會比較新舊狀態,如果兩次變更檢測(開發環境下)的結果不一致就會報錯,例如:
Expression has changed after it was checked
這也就解釋了為什么在子組件中更改了父組件的值會報錯。
但是!在前面的兩個例子中我們都在子組件中更改了父組件的值,只有第一個報錯,第二個是可以正常更新的,如果你也同樣很疑惑這中間真正的差異點在哪里,那么接著往下閱讀吧~
先上結論:
更新所有子組件的綁定屬性
調用所有子組件的 OnChanges,OnInit,DoCheck,AfterContentInit 生命周期鉤子
更新當前組件的 DOM
子組件觸發變更檢測
調用所有子組件的的 AfterViewInit 的生命周期鉤子
這里我們不關注于太細的細節(不用好奇為什么是這樣的順序,只要記住 Angular 里就是這樣設定的就可以了,如果有大佬想談談 Angular 在這部分的設計思想,歡迎在評論區留言探討~)
第一個例子中,父組件 parent 給子組件 child 傳入了輸入屬性 name,且子組件在 ngOnInit 中更新了父組件的 name 屬性,也就是說這段代碼**違背了檢測順序(**在順序的第二步中操作了第一步)!
<p>{{ name }}<p> <child [name]="name"></child>
而在第二個例子中,就算子組件在 ngOnInit 中也更新了父組件的 name 屬性,但是由于父組件parent 中沒有給子組件 child 綁定輸入屬性 name,不會出現與違背變更檢測隊列順序的情況,所以就可以正常運行。
<p>{{ name }}<p> <child></child>
這個時候再去看看 stackoverflow 上的高贊回答 是不是就清晰明了很多,按照上述的檢測順序,我們會發現只要父組件中對子組件做了屬性綁定,不管是在 OnChanges,OnInit,DoCheck,AfterContentInit 和 AfterViewInit 中的任意一個聲明周期鉤子中執行下述代碼都會報錯。
this.parentCmpt.name = 'child'
好了,到這里我們已經明白了這種錯誤發生的真正原因,但是我還是要提醒一下,這種錯誤只會在開發環境下觸發,生產環境下會調用 enableProdMode()
,變更檢測次數會從 2 降到 1,這部分在 Angular 源碼當中也有描述。
當然你不能因為這個 bug 就強制在開發環境下使用生產模式...
ChangeDetectionStrategy
默認為 Default,也就是父組件的 CD 會觸發子組件的 CD,但是很顯然有些情況下我們可以自行判斷出某些子組件在父組件 CD 時并不用觸發,而 OnPush
則是 Angular 為開發者提供的一便捷操作方式。
用動圖來表示就是:查看鏈接
知名的 Angular 開源組件庫 ng-zorro 就使用了大量的 OnPush 策略,這也是 Angular 性能優化的方法之一。
Angular 給每個組件都關聯了一份組件視圖,通過 ChangeDetectorRef
可以拿到相關聯的視圖,在定義中我們可以看到:
export declare abstract class ChangeDetectorRef { abstract checkNoChanges(): void; abstract detach(): void; abstract detectChanges(): void; abstract markForCheck(): void; abstract reattach(): void; }
觀察下面的動圖,被 detached
的組件將不會被檢查變更。
而 reattach
則可以讓被 detached
的組件重新可以被檢測到變更。
reattach
只會重新啟用對當前組件的變更檢測,但是如果父組件沒有啟動變更檢測,那么 reattach
并不會起作用,而 markForCheck
可以很好地解決這個問題。
這一點在 ng-zorro 的源碼中可以了解一二。
例如在 nz-anchor 組件中更改 nz-anchor-link 組件的 active 屬性時,由于本身 ChangeDetectionStrategy
為 OnPush
,那么就需要激活 markForCheck 來重新啟用檢測。具體寫法可以查看 github 中的源代碼。
nz-anchor.component.ts
nz-anchor-link.component.ts
用動圖來展示則是這樣,注意觀察設置了 MFC 的前后變化
這個方法如同字面意思一樣很好理解,就是觸發一次變更檢測啦,還記得本文中的第一個例子嗎,我們不手動觸發 tick()
,而是觸發 detechtChanges()
也是可以達到效果的。
以上是“Angular中如何實現變更檢測”這篇文章的所有內容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內容對大家有所幫助,如果還想學習更多知識,歡迎關注億速云行業資訊頻道!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。