iT邦幫忙

2026 iThome 鐵人賽

DAY 4
3
Modern Web

Angular 22 Signal 進化論系列 第 4

Day 4:Angular 怎麼知道誰正在讀 Signal?從 Reactive Context 理解依賴追蹤

  • 分享至 

  • xImage
  •  

前面我們看過,當 Template 讀取 Signal 時,Angular 會記住「誰依賴了誰」;當 Signal 發生變化後,也會透過 Reactive Graph 通知相關的 Consumer。

不過這裡其實還有一個很重要的問題:Angular 到底怎麼知道「現在是哪個 Consumer 正在讀取這個 Signal」?

這部分和 Reactive Context 有關。今天會先從它為什麼存在、通常會出現在哪些地方開始理解。

Angular Reactive Context 是什麼?

當程式執行在 Reactive Context 中時,Angular 會知道「現在是哪個 Consumer 正在執行」,並追蹤這段期間讀取了哪些 Signal。

像是 computed()linkedSignal()effect(),或 Template 執行時,都可能進入 Reactive Context。當其中讀取到某個 Signal 時,Angular 就會在這個 Producer 和目前的 Consumer 之間建立依賴關係,形成 Reactive Graph。

例如:

const count = signal(1);

effect(() => {
  console.log(count());
});

effect() 執行時,它會成為目前的 Consumer。接著 callback 裡讀取到 count(),Angular 就會在 count 這個 Producer 和 effect 這個 Consumer 之間建立一條 ReactiveLink

這條 ReactiveLink 不只用來表示兩者之間的依賴關係,也會保存像是 Consumer 上次讀取 Producer 時看到的版本。後續 Angular 就能利用這些資訊,判斷資料從上次讀取之後是否真的發生變化。

這個部分的關係大致如下:

Angular Reactive Context 整體流程

Reactive Context 的認知重點

依賴關係在 Runtime 執行時動態建立的,不是在 Build Time 就決定好

Reactive Context 裡的依賴關係,不是 Angular 一開始就全部決定好,而是會看每一次實際執行時,到底讀到了哪些 Signal。

像是 computed() 裡如果有 if 判斷,Angular 只會追蹤這次真的有跑到的那一條路徑。下一次重新計算時,如果走到不同的分支,依賴關係也會跟著改變。

例如:

const showPrice = signal(true);
const price = signal(100);
const quantity = signal(2);

const result = computed(() => {
  if (showPrice()) {
    return price();
  }

  return quantity();
});
  • showPrice()true 時,這次會讀取 showPriceprice,所以 Angular 會追蹤這兩個 Signal。
  • showPrice()false 時,重新計算後就會改成讀取 showPricequantity,依賴關係也會跟著調整。

所以 Angular 會根據每一次實際執行時讀到哪些 Signal,動態更新目前的依賴關係。這也是為什麼 computed() 的依賴不是固定不變的。

Reactive Context 只會追蹤同步執行期間讀取到的 Signal

Reactive Context 只存在於目前這段同步執行的過程中。當 callback 的同步程式執行結束後,Angular 就會把目前的 Consumer 還原。

所以如果中間跨過 await 這類非同步邊界,等程式之後再繼續執行時,原本的 Reactive Context 已經結束了。這時才讀取到的 Signal,就不會再被原本的 Consumer 追蹤。

例如:

const count = signal(0);

effect(async () => {
  await Promise.resolve();

  console.log(count());
});

這裡的 count() 是在 await 之後才讀取,因此不會被這個 effect() 追蹤成依賴。

如果希望 count 被追蹤,就要在跨過非同步邊界之前先讀取:

effect(async () => {
  const currentCount = count();

  await Promise.resolve();

  console.log(currentCount);
});

這樣 count() 是在 Reactive Context 還存在時被讀取,Angular 才能把它記成這個 effect() 的依賴。


但身為 Angular 工程師,看到這裡可能會馬上想到另一個常見情境:那如果換成 RxJS 呢?

類似的狀況也會出現在 Subject 或 Observable。像下面這種寫法,雖然 subscribe() 是在 effect() 裡建立的,但真正執行 callback 的時間,取決於資料什麼時候被送進來:

const count = signal(0);
const subject = new Subject<void>();

effect(() => {
  subject.subscribe(() => {
    console.log(count());
  });
});

如果 subject.next() 是在這個 effect() 執行結束之後才被呼叫,那麼 subscriber callback 執行時,原本的 Reactive Context 已經結束了,因此這時才讀取到的 count(),就不會被這個 effect() 追蹤成依賴。

如果把前面的幾個情境放在一起看,Reactive Context 的追蹤方式大概就是這樣:

Angular Signal 在 Reactive Context 被追蹤的情形

effect() 中的 untracked() 與 Reactive Context

這裡也可以延伸出一個很實用的問題:

為什麼在 effect() 裡使用 untracked(),就可以避免某個 Signal 被追蹤成依賴?

原因是當 untracked() 執行時,Angular 會暫時把目前的 activeConsumer 設成 null。這樣在 untracked() 裡讀取 Signal 時,Angular 就找不到「現在是哪個 Consumer 在讀」,因此不會建立這次的依賴關係。

例如:

const count = signal(0);
const name = signal('Antonio');

effect(() => {
  console.log(name());
  console.log(untracked(count));
});

這裡的 name() 是直接在 effect() 的 Reactive Context 中被讀取,所以會被 Angular 追蹤。

count() 被包在 untracked() 裡,讀取當下的 activeConsumer 會暫時變成 null,因此不會建立 counteffect 的依賴。

untracked() 執行結束後,Angular 會再把原本的 activeConsumer 還原,後面的依賴追蹤也就能繼續正常進行。

Reactive Context 也會影響 Signal 能不能被寫入

前面主要都在談「讀取 Signal」時,Angular 怎麼利用 activeConsumer 建立依賴。

不過 activeConsumer 不只和讀取有關,它也會影響目前這個 Consumer 到底能不能寫入 Signal。

Day 3 看過的 signalSetFn() 中,一開始就會先呼叫 producerUpdatesAllowed()

export function producerUpdatesAllowed(): boolean {
  return activeConsumer?.consumerAllowSignalWrites !== false;
}

這裡會進一步檢查目前 activeConsumer 身上的 consumerAllowSignalWrites,判斷這個 Consumer 是否允許寫入 Signal。

  • computed():計算過程中不允許透過 set()update() 修改其他 Signal,Angular 會直接阻止這次操作。
  • effect():目前允許寫入 Signal。不過如果修改的是自己正在追蹤的 Signal,就可能讓 effect() 被反覆觸發,甚至形成循環更新,所以這種寫法還是要特別小心。

反過來說,如果是在一般函式、事件處理或其他沒有 activeConsumer 的情況下,producerUpdatesAllowed() 就會回傳 true,因此可以正常寫入 Signal。

所以 Angular 並不是單純判斷「現在是不是在 Reactive Context 裡」,還會再進一步確認目前是哪個 Consumer,以及這個 Consumer 到底允不允許寫入 Signal。

有些 API 反而不能在 Reactive Context 中執行

前面看到 Reactive Context 可以幫 Angular 追蹤依賴,也會影響 Signal 能不能被寫入。

不過還有另一種情況:有些 API 本身就不適合在 Reactive Context 裡被呼叫。

最常見的例子,就是不能在一個正在執行的 effect() 裡,再動態建立另一個 effect();另外像 RxJS interop 的 toSignal() 也有類似的限制。

原因是這類 API 通常會建立新的副作用、訂閱或其他資源。如果每次 Reactive Context 重新執行時都再建立一次,就可能出現重複註冊的情形。

如果自己也要設計這類函式,就可以使用 Angular 提供的 assertNotInReactiveContext()

import { assertNotInReactiveContext } from '@angular/core';

export function startPolling() {
  assertNotInReactiveContext(startPolling);

  // 建立計時器或訂閱
}

它會檢查目前是不是正在 Reactive Context 中執行。如果目前存在 activeConsumer,就會直接丟出錯誤,避免這個函式在不適合的時機被呼叫。

所以從這裡也可以看到,activeConsumer 不只是 Angular 拿來建立 Signal 依賴關係的資訊,也會進一步影響其他 Reactive API 的執行行為。

本日結語

Angular 會透過 activeConsumer 知道現在是哪個 Consumer 正在執行,並在 Signal 被讀取的當下建立依賴關係。

因此依賴不是寫死的,而是會跟著每一次實際執行的路徑動態調整;跨過 await 或非同步 callback 之後,原本的 Reactive Context 就已經結束,而 untracked() 則是主動暫時把 activeConsumer 清掉,讓這次讀取不被追蹤。

另外,Reactive Context 不只影響「怎麼追蹤 Signal」,也會進一步影響目前的 Consumer 能不能寫入 Signal,以及某些 API 能不能在這個執行環境裡被呼叫。

資料來源


上一篇
Day3:從 set()、update() 看懂 Angular Signal 的基本使用與更新機制
下一篇
Day 5:從 Reactive Graph 到 View Tree,看 Angular 如何找到真正需要更新的畫面
系列文
Angular 22 Signal 進化論6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言