細說Angular中的變化檢測(Change Detection)

買賣虛擬貨幣

在使用Angular進行開發中,我們常用到Angular中的繫結——模型到檢視的輸入繫結、檢視到模型的輸出繫結以及檢視與模型的雙向繫結。而這些繫結的值之所以能在檢視與模型之間保持同步,正是得益於Angular中的變化檢測。

變化檢測是什麼?

簡單來說變化檢測就是Angular用來檢測檢視與模型之間繫結的值是否發生了改變,當檢測到模型中繫結的值發生改變時,則同步到檢視上,反之,當檢測到檢視上繫結的值發生改變時,則回撥對應的繫結函式。

什麼情況下會引起變化檢測?

變化檢測的關鍵在於如何最小粒度地檢測到繫結的值是否發生了改變,那麼在什麼情況下會導致這些繫結的值發生變化呢?我們可以看一下我們常用的幾種場景:

我們在檢視上透過插值表示式繫結了MyApp中的name屬性,當點選按鈕時,改變了name屬性的值,這時就導致了繫結的值發生了變化。再來看另外一種場景:

我們在MyApp這個元件的生命週期鉤子函式里向伺服器端傳送了一個Ajax請求,當這個請求返回結果時,同樣會改變當前檢視上繫結的name屬性的值。類似的,我們還可能設定一些定時任務,這些定時任務也可能會改變與檢視繫結的值:

其實,我們不難發現上述三種情況都有一個共同點,即這些導致繫結值發生改變的事件都是非同步發生的。如果這些非同步的事件在發生時能夠通知到Angular框架,那麼Angular框架就能及時的檢測到變化。

那麼Angular框架如何才能獲知到這些非同步事件的發生呢?我們不妨來看一看非同步事件在Javascript中執行的過程:

左邊表示將要執行的程式碼,這裡的stack表示Javascript的執行棧,而webApi則是瀏覽器中提供的一些Javascript的API,TaskQueue表示Javascript中任務佇列,因為Javascript是單執行緒的,非同步任務在任務佇列中執行。

當上述程式碼在Javascript中執行時,首先func1 進入執行棧,func1執行完畢後,setTimeout進入執行棧,執行setTimeout過程中將回撥函式cb 加入到任務佇列,然後setTimeout出棧,接著執行func2函式,func2函式執行完畢時,執行棧為空,接著任務佇列中cb 進入執行棧得到執行。可以看出非同步任務首先會進入任務佇列,當執行棧中的同步任務都執行完畢時,非同步任務進入執行棧得到執行。如果這些非同步的任務執行前與執行後能提供一些鉤子函式,透過這些鉤子函式,Angular便能獲知非同步任務的執行。

事實上,Angular正是使用Zonejs來做到的。Zonejs透過猴子補丁的方式,對webApi中的一些非同步任務的API在執行時進行替換,替換後的API提供了一些鉤子函式。

變化檢測是個什麼樣的過程?

透過上面的介紹,我們大致明白了變化檢測是如何被觸發的,那麼Angular中的變化檢測是如何執行的呢?首先我們需要知道的是,對於每一個元件,都有一個對應的變化檢測器;即每一個Component都對應有一個changeDetector,我們可以在Component中透過依賴注入來獲取到changeDetector。而我們的多個Component是一個樹狀結構的組織,由於一個Component對應一個changeDetector,那麼changeDetector之間同樣是一個樹狀結構的組織。最後我們需要記住的一點是,每次變化檢測都是從樹根開始的。

枯燥無味的理論到此結束,下面透過一些例子來直觀的感受一下。

Main.component.ts :

上述程式碼中,MainComponent透過<movie></movie> 標籤嵌入了MovieComponent,從樹狀結構上來說,MainComponent是MovieComponent的根節點,而MovieComponent是MainComponent的葉子節點。當我們點選MainComponent的第一個button時,會回撥到changeActorProperties方法,然後會觸發變化檢測的執行。首先變化檢測從MainComponent開始:

  • 檢測slogan 值是否發生了變化:沒有發生變化

  • 檢測 title 值是否發生了變化:沒有發生變化

  • 檢測 actor 值是否發生了變化:沒有發生變化

你可能對於 actor的檢測結果感到疑惑,不是明明改變了actor的屬性值嗎?實質上在對actor檢測時只檢測actor 本身的引用值是否發生了改變,改變actor的屬性值並未改變actor 本身的引用,因此是沒有發生變化。而當我們點選MainComponent的第二個button ,重新new了一個 actor ,這時變化檢測才會檢測到 actor發生了改變。

然後變化檢測進入到葉子節點MovieComponent:

  • 檢測title 值是否發生了改變:沒有發生變化

  • 檢測actor.firstName 是否發生了變化:發生了改變

  • 檢測actor.lastName 是否發生了改變:發生了改變

因為MovieComponent再也沒有了葉子節點,所以變化檢測將更新DOM,同步檢視與模型之間的變化。

看到這裡你可能會想,這機制未免也有點太簡單粗暴了吧,假如我的應用中有成百上千個Component,隨便一個Component 觸發了檢測,那麼都需要從根節點到葉子節點重新檢測一遍。彆著急,Angular 的開發團隊已經考慮到了這個問題,上述的檢測機制只是一種預設的檢測機制,Angular還提供一種OnPush的檢測機制,還是同樣的例子,下面看一下OnPush檢測機制是咋樣的:

Main.component.ts :

與上面的程式碼相比,只在@Component中新增了:

即將檢測機制設定為OnPush。同樣的,當我們點選第一個button時,將會發生如下變化檢測:

  • 檢測slogan 值是否發生了變化:沒有發生變化

  • 檢測 title 值是否發生了變化:沒有發生變化

  • 檢測 actor 值是否發生了變化:沒有發生變化

好,變化檢測到此結束,不會再進入到 MovieComponent 中了。這正是OnPush與Default之間的差別:當檢測到與子元件輸入繫結的值沒有發生改變時,變化檢測就不會深入到子元件中去。那麼當我們點選MainComponent中的第二個按鈕時,由於改變了actor本身而不是它的屬性值,那麼就會檢測到actor的變化,進而繼續進入到MovieComponent 進行變化檢測。所以,當你使用了OnPush檢測機制時,在修改一個繫結值的屬性時,要確保同時修改到了繫結值本身的引用。但是每次需要改變屬性值的時候去new一個新的物件會使得程式碼很難看,並且有時候你難以保證你一定記得這麼做,恩,immutable.js 你值得擁有!

另外一個問題就是,當我們使用OnPush並且輸入繫結的是一個Observable物件時,怎麼才能檢測到當訂閱的事件發生時引起的繫結的值的發生了改變呢?比如下面這個元件:

輸入繫結 addItemStream 是一個Observable物件,Observable物件本身是不會變化的,只有當訂閱的事件到達時,才會去修改count的值。如果使用OnPush 那麼檢測就不會進入到CountComponent。解決的辦法很簡單,只需在修改count的值後做一個標記(markForCheck),那麼變化檢測就會沿著CountComponent所在的樹枝進行變化檢測。

總結

總結來說,Angular中變化檢測器是樹型結構的組織,與元件樹結構相對應,預設情況下,當一個元件引發了變化檢測時,檢測是從樹根開始一直檢測到樹節點。當你設定某個元件的檢測策略是 OnPush 時,如果該元件的輸入繫結沒有發生變化時,那麼檢測就不會進入到該元件。當元件樹變的很龐大時,常用這種辦法來提高應用的效能。

免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读

;