iT邦幫忙

2026 iThome 鐵人賽

DAY 5
2
Modern Web

Angular 22 Signal 進化論系列 第 5

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

  • 分享至 

  • xImage
  •  

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

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

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

Angular Consumer 跟 View Tree

Change Detection 會沿著 View Tree 往下檢查

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

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

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

AppComponent
└── LayoutComponent
    └── ProductComponent

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

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

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

Reactive GraphView Tree 之間的關係大致如下:

Angular Reactive Graph 與 View Tree

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

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

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

  producerNotifyConsumers(node);

  node.consumerMarkedDirty?.(node);
}

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

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

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

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

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

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

假設目前的 View Tree 是:

AppComponent
└── LayoutComponent
    └── ProductComponent

而真正受到影響的是 ProductComponent,Angular 就會往上處理 LayoutComponentAppComponent

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

概念上可以先理解成:

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 還要繼續往下走。

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

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

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

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

AppComponent
HasChildViewsToRefresh
        ↓
LayoutComponent
Consumer dirty
HasChildViewsToRefresh
        ↓
ProductComponent
Consumer dirty

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

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

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

例如:

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

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

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

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

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

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;
}

整體流程可以大致理解如下:
Angular 變更偵測流程圖

所以從 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 valueLView 中保存的舊值進行比較,只有不同時才進一步更新 DOM。

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

資料來源


上一篇
Day 4:Angular 怎麼知道誰正在讀 Signal?從 Reactive Context 理解依賴追蹤
下一篇
Day6:Resource API:把非同步資料帶進 Signal
系列文
Angular 22 Signal 進化論6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言