這篇會依照上一篇結尾的概念延續,基本上就是透過「閉包 + 解構賦值概念」,做出的狀態暫存機制,範例如下:
export type Signal<T> = {
    get(): T;
    set(next: T | ((prev: T) => T)): void;
};
export function signal<T>(initial: T): Signal<T> {
    let value = initial;
    const get = () => value;
    const set = (next: T | ((p: T) => T)) => {
        const nxtVal = typeof next === 'function' ? (next as (p: T) => T)(value) : next;
        const isEqual = Object.is(value, nxtVal);
        if (!isEqual) value = nxtVal;
    };
    return { get, set };
}
還記得這張圖嗎?
我們還缺少了一個可以存儲依賴關係的 Observers,也就是要完成 Signal 最後一塊拼圖。
const { get, set, subscribe} = signal()
withObserver / track 工具:用「讀取行為」自動建立訂閱邊界(dependency edges)首先我們先來釐清追蹤的概念,能被追蹤的角色代表者資訊的發起者,又或者叫被訂閱,舉個明顯的例子就是 Signal;那觀察的概念就很簡單了,又或者叫訂閱者,舉個明顯的例子就是 Effect。
那麼簡單的二分法,我們大概可以得到以下結論:
subs: Set<Observer>(誰訂閱了我)deps: Set<Trackable>(我訂閱了誰)
從來源者的角度來看,如下圖:
從 Observer 的角度來看,如下圖:
這是一個雙向圖:來源知道訂閱者,訂閱者知道來源。
對了,這邊探討的話會應用到 圖(Graph) 的資料結構概念,最好還是釐清一下基本原理,這樣才比較不會看不懂後續的描述,因為後面優化的部分就會頻繁提及這些概念。
按照上述的概念可以簡化成以下的型別:
export interface Trackable {
    subs: Set<Observer>;
};
export interface Observer {
    deps: Set<Trackable>;
};
我們透過 currentObserver + track 的機制運行:
let currentObserver: Observer | null = null;
export function withObserver<T>(obs: Observer, fn: () => T): T {
      const prev = currentObserver;
      const currentObserver = obs;
      try {
          return fn();
      } finally {
          currentObserver = prev;
      }
}
export function track(dep: Trackable) {
    if (!currentObserver) return;
    dep.subs.add(currentObserver);
    currentObserver.deps.add(dep);
}
有了上面前面的機制,我們與開頭的基本閉包 signal 範例結合,實現出一個可訂閱的最小核心:
type Kind = 'signal' | 'computed' | 'effect';
export interface Node {
  kind: Kind;
  deps: Set<Node>; // 我依賴了誰(只對 computed/effect 會用到)
  subs: Set<Node>; // 誰依賴我(signal/computed 都會被訂閱)
}
// 不變式:signal 不能有 deps;effect 不提供 subs
export function link(from: Node, to: Node) {
  if (from.kind === 'signal') {
    throw new Error('Signal nodes cannot depend on others');
  }
  from.deps.add(to);
  to.subs.add(from);
}
export function unlink(from: Node, to: Node) {
  from.deps.delete(to);
  to.subs.delete(from);
}
// 追蹤工具:在「observer context」中讀取時自動建邊界(只建圖,不通知)
let currentObserver: Node | null = null;
export function withObserver<T>(obs: Node, fn: () => T): T {
  const prev = currentObserver;
  currentObserver = obs;
  try {
    return fn();
  } finally {
    currentObserver = prev;
  }
}
function track(dep: Node) {
  if (!currentObserver) return; // 非追蹤階段就只是一般讀取
  link(currentObserver, dep); // 觀察者 -> 被觀察者
}
// 物件回傳,便於解構; 將 Object.is 先抽出來成 equals,下一篇用於通知判斷
type Comparator<T> = (a: T, b: T) => boolean;
const defaultEquals = Object.is;
export function signal<T>(initial: T, equals: Comparator<T> = defaultEquals) {
  // 單一節點 + 私有值
  const node: Node & { kind: 'signal'; value: T; equals: Comparator<T> } = {
    kind: 'signal',
    deps: new Set(), // 永遠保持空集合(由 link() 保證)
    subs: new Set(),
    value: initial,
    equals,
  };
  const get = () => {
    track(node);
    return node.value;
  };
  const set = (next: T | ((prev: T) => T)) => {
    const nxtVal = typeof next === 'function' ? (next as (p: T) => T)(node.value) : next;
    if (node.equals(node.value, nxtVal)) return;
    node.value = nxtVal;
    // 本篇只談訂閱建圖,不做 dirty/通知;下一篇再接續
  };
  // 事件式顯式訂閱(用來對照宣告式追蹤);回傳取消訂閱
  const subscribe = (observer: Node) => {
    if (observer.kind === 'signal') {
      throw new Error('A signal cannot subscribe to another node');
    }
    link(observer, node);
    return () => unlink(observer, node);
  };
  return { get, set, subscribe, peek: () => node.value };
}
為什麼既有 track 又提供 subscribe?
track() 是宣告式依賴:在「追蹤區塊」內讀到誰,就自動訂閱誰(之後給 computed/effect 用)。subscribe() 是命令式訂閱:你可以手動把某個 Observer 掛到某個 signal 上,對照傳統事件訂閱的做法,便於相容於不同框架。peek() 方便測試與接其他框架使用。| 面向 | 事件訂閱( subscribe(cb)) | 依賴追蹤( track/withObserver) | 
|---|---|---|
| 目標 | 值改變時 立即呼叫 callback | 建立 資料流圖,供之後的重算/排程使用 | 
| 建立方式 | 手動註冊與移除 | 在「讀取階段」自動追蹤 | 
| 適用情境 | I/O、日誌、橋接第三方系統 | computed/effect的來源收集與傳染式失效 | 
| 生命週期 | 由使用者自己管理 | 之後可由 computed/effect的生命周期自動管理 | 
到這裡,我們已完成「signal + 訂閱機制」:
在 withObserver(() => a.get()) 的追蹤區塊裡,會自動建立 Observer → Trackable 的依賴邊界。
本篇僅建圖,不觸發任何重新執行。
下一篇要做的事很單純:實作 effect,讓圖真正「動」起來。
kind: 'effect' 的節點,第一次執行時用 withObserver 收集依賴。signal.set() 發生時,通知對應的 effect,在微任務中合併重新執行。dispose / onCleanup:重跑前先移除舊依賴並執行清理。