正在載入索引…

Day 5:從 Reactive Graph 到 View Tree,看 Angular 如何找到真正需要更新的畫面

4.4k 字,約 4 分鐘

2026 iThome 鐵人賽:Angular 22 Signal 進化論。 原文發表於 iThome,也可以在下方留言。

文章目錄
  1. Change Detection 會沿著 View Tree 往下檢查
  2. Consumer 被標記為 dirty 後會發生什麼?
  3. Angular 怎麼在 View Tree 中留下檢查路徑?
  4. Change Detection 怎麼走到真正需要重新檢查的 View?
  5. 重新執行 Template,不代表 DOM 一定會更新
  6. 本日結語
    1. 資料來源

前面我們已經知道,當 Signal 的值發生變化後,Angular 會透過 Reactive Graph 找到受到影響的 Consumer。

不過找到 Consumer 只是第一步。對 Template 來說,Angular 還需要知道這個 Consumer 對應到哪一個 View,並在 View Tree 中保留一條可以走到它的路徑。

這樣下一次執行 Change Detection 時,Angular 才能沿著這條路找到真正需要重新檢查的 View,而不是把整個分支直接略過。

Change Detection 會沿著 View Tree 往下檢查

Reactive Graph 主要負責記錄 Signal 和 Consumer 之間的依賴關係。

但當 Angular 真正開始執行 Change Detection 時,會沿著 View Tree 往下檢查各個 View。

可以先把 View Tree 想成和「元件樹」很接近的概念,例如:

text
AppComponent
└── LayoutComponent
    └── ProductComponent

補充:不過兩者並不完全一樣,因為 View Tree 除了元件之外,也可能包含實際建立出來的 Embedded View,例如 @if、@for 或 ng-template 產生的 View。

假設真正受到 Signal 變化影響的是最下面的 ProductComponent,Angular 執行 Change Detection 時,還是需要從上層沿著 View Tree 往下走,才能到達這個 View。

所以即使 Angular 已經透過 Reactive Graph 找到受到影響的 Consumer,還是需要在 View Tree 上留下對應的路徑,避免還沒走到目標 View,就先把這個分支略過。

Reactive Graph 跟 View Tree 之間的關係大致如下:

Consumer 被標記為 dirty 後會發生什麼?

當 Consumer 被標記為 dirty 時,Angular 除了會先設定 dirty = true,最後還會執行這個 Consumer 自己的 consumerMarkedDirty():

ts
export function consumerMarkDirty(node: ReactiveNode): void {
  node.dirty = true;

  producerNotifyConsumers(node);

  node.consumerMarkedDirty?.(node);
}

對 Template 對應的 ReactiveLViewConsumer 來說,接下來就會回到它所屬的 View 繼續處理。

Angular 怎麼在 View Tree 中留下檢查路徑?

這時 consumerMarkedDirty() 會進一步呼叫 markAncestorsForTraversal():

ts
consumerMarkedDirty: (node: ReactiveLViewConsumer) => {
  markAncestorsForTraversal(node.lView!);
}

這裡的 node.lView,就是這個 Consumer 所對應的 View。

Angular 接著會從這個 View 往上找到祖先,並在沿途留下標記。

假設目前的 View Tree 是:

text
AppComponent
└── LayoutComponent
    └── ProductComponent

而真正受到影響的是 ProductComponent,Angular 就會往上處理 LayoutComponent 和 AppComponent。

這裡不是把所有祖先 View 都標記成「需要重新執行 Template」,而是告訴 Angular:這個分支下面還有 View 要處理,之後執行 Change Detection 時還要繼續往下。

概念上可以先理解成:

text
AppComponent
HasChildViewsToRefresh
        ↓
LayoutComponent
HasChildViewsToRefresh
        ↓
ProductComponent
Consumer dirty

補充:Angular 內部會透過 HasChildViewsToRefresh 這個 flag 來記錄這件事情,代表這個 View 的子樹裡還有其他 View 需要處理,之後執行 Change Detection 時要繼續往下檢查。

至於目前這個 View 自己是否需要重新檢查,則要看它本身的狀態。HasChildViewsToRefresh 關心的是「下面還要不要繼續往下走」,而 Consumer 的 dirty 則表示「目前這個 View 自己是否可能需要進一步檢查」。

Change Detection 怎麼走到真正需要重新檢查的 View?

路徑留下來之後,下一次執行 Change Detection 時,Angular 就會根據這些標記沿著 View Tree 往下處理。

這時可以先看兩個狀態:

  • Consumer 被標記為 dirty:代表這個 View 本身可能需要重新檢查,Angular 會再確認它依賴的 Producer 是否真的發生變化。
  • 帶有 HasChildViewsToRefresh:代表這個 View 的下面還有其他 View 需要處理,所以 Change Detection 還要繼續往下走。

以前面的例子來看,AppComponent 和 LayoutComponent 主要是保留往下走的路徑。

當 Angular 走到 ProductComponent 時,因為它的 Consumer 已經被標記為 dirty,就會進一步確認依賴的 Producer 是否真的有變化,再決定這個 View 要不要重新檢查。

而且 dirty 和 HasChildViewsToRefresh 並不是互斥的,同一個 View 也可能同時存在這兩種狀態。

例如 LayoutComponent 自己讀取的另一個 Signal 也發生變化時,就可能變成:

text
AppComponent
HasChildViewsToRefresh
        ↓
LayoutComponent
Consumer dirty
HasChildViewsToRefresh
        ↓
ProductComponent
Consumer dirty

這時 LayoutComponent 自己也可能需要重新檢查;同時因為它還帶有 HasChildViewsToRefresh,代表下面的 ProductComponent 也需要處理,所以 Change Detection 還是會繼續往下走。

重新執行 Template,不代表 DOM 一定會更新

當 Angular 確認某個 View 需要重新檢查後,就會重新執行其中的 Template binding。

例如:

html
<p>{{ count() }}</p>

當這個 View 被重新檢查時,Angular 會再次取得 count() 的結果,產生這一次的 binding value。

不過 Template 重新執行,並不代表 DOM 就一定會跟著修改。

Angular 會把每個 binding 的值保存在 LView 對應的位置。重新計算之後,再拿這次的結果和上一次保存的值進行比較。

簡化來看,底層的 bindingUpdated() 大致會做這件事情:

ts
export function bindingUpdated(
  lView: LView,
  bindingIndex: number,
  value: any,
): boolean {
  const oldValue = lView[bindingIndex];

  if (Object.is(oldValue, value)) {
    return false;
  }

  lView[bindingIndex] = value;
  return true;
}

整體流程可以大致理解如下:

所以從 Signal 發生變化到最後更新畫面,中間其實會經過好幾個階段。

Angular 會先透過 Reactive Graph 找到受到影響的 Consumer,再透過 View Tree 保留 Change Detection 需要經過的路徑。

當 Change Detection 走到真正需要重新檢查的 View 時,才會重新執行對應的 Template binding。

因為 Angular 在 Template 編譯階段,就已經知道有哪些 binding,以及它們各自對應的位置,所以可以直接重新計算對應的值,再和 LView 中保存的舊值進行比較。

只有當新舊 binding value 真的不同時,Angular 才會進一步更新對應的 DOM。

本日結語

回到 Day 2 整理的簡化流程,今天其實把後半段一次走完了:

  • Step 3:標記受到影響的 View 與祖先路徑:Consumer 被標記為 dirty 後,會透過 markAncestorsForTraversal() 替祖先 View 加上 HasChildViewsToRefresh,留下一條往下走的路徑。
  • Step 4:Angular 執行 Change Detection:沿著 View Tree 往下走,帶有標記的分支會繼續往下,真正受到影響的 View 才會重新檢查。
  • Step 5:比較 binding value 並更新 DOM:重新執行 Template binding 後,會將新的 binding value 和 LView 中保存的舊值進行比較,只有不同時才進一步更新 DOM。

所以 Signal 帶來的不只是「值變了會通知」,它也讓 Angular 在執行 Change Detection 時知道該往哪裡走、哪些分支可以略過,進一步縮小需要檢查的範圍。

資料來源

留言

留言使用 GitHub 帳號登入(Giscus),留言會存在 GitHub Discussions。