graph.ts:Node{ kind, deps, subs }、link/unlink、withObserver/track(沿用實作 Effect(I))EffectRegistry:Effects.get/set/delete(node: Node)(沿用實作Effect (II))signal.ts:set() 會遍歷 subs,對 effect → Effects.get(sub)?.schedule()(下面會加上 computed 的分支)TL;DR:等同 Vue 的
computed/ Solid 的createMemo;不是 React 的useMemo。
它是反應式資料流圖上的節點,自動追蹤依賴,且被讀取時才重算(lazy),不綁定任何元件生命週期。
| 面向 | 本文的 computed(signals) | Vue computed | Solid createMemo | React useMemo | 
|---|---|---|---|---|
| 依賴宣告 | 自動追蹤(讀誰訂誰) | 自動 | 自動 | 手動 deps array | 
| 何時計算 | 讀取時若 stale才重算(lazy) | lazy | lazy | 渲染時依 deps array | 
| 生命週期 | 獨立於元件 | 隨實例 | 隨 root | 綁元件 render | 
| 在資料流圖中的角色 | Observer + Trackable(有 deps / subs) | 同左 | 同左 | 不是 reactive node(只是 render 快取) | 
| 是否適合放副作用 | 否 | 否 | 否 | 否 | 
const a = ref(1);
const b = ref(2);
const sum = computed(() => a.value + b.value);
const [a] = createSignal(1);
const [b] = createSignal(2);
const sum = createMemo(() => a() + b());
const a = signal(1);
const b = signal(2);
const sum = computed(() => a.get() + b.get()); // 自動追蹤,lazy 重算
const [a, setA] = useState(1);
const [b, setB] = useState(2);
// 這是 render 階段快取,不參與 reactive graph
const sum = useMemo(() => a + b, [a, b]);
接下來你看到的 computed,就是「讀取才算、算完快取、圖上有上下游」的那種。
computed 同時是 Observer(會去依賴別人)與 Trackable(可被依賴):既有 deps 又有 subs
stale: boolean:是否需要重算(髒標記)computing: boolean:重入保護(循環依賴偵測)equals(a, b):結果相等時不更新快取(預設 Object.is)下圖箭頭代表「我依賴了誰」。computed 既是 Observer(有 deps)也是 Trackable(有 subs)。
signal:只被依賴(沒有 deps)。effect:只去依賴(沒有 subs)。computed:上下游各一手,夾在中間。set 到重跑(含 Registry)signal.set 只推送失效(作髒標記 computed、排程 effect);真正的「計算」等被讀取時才發生。
EffectRegistry(Symbol 或 WeakMap 皆可)。set 只重跑一次。computed.get 的懶(Lazy)計算流程被讀取時才檢查 stale。髒了就重算:先解除舊依賴,再在追蹤上下文內執行並快取。
signal.set 對 computed 只做 markStale,對 effect 交給 Effects.get(node)?.schedule()。computed.get() 檢查 stale 才重算與更新依賴。computing 期間再讀自己會丟錯,避免自觸發。// computed.ts
import { link, unlink, withObserver, track, type Node } from "./graph";
import { SymbolRegistry as Effects } from "./registry";
type Comparator<T> = (a: T, b: T) => boolean;
const defaultEquals = Object.is;
/** 供 signal.set() 與其它 computed 標髒標記使用 */
export function markStale(node: Node) {
  if (node.kind !== "computed") return;
  const c = node as Node & { stale: boolean };
  if (c.stale) return; // 已經髒就別重複傳染
  c.stale = true;
  // 向下傳染:讓依賴此 computed 的節點一併反應
  for (const sub of node.subs) {
    if (sub.kind === "computed") {
      markStale(sub); // 傳染給下游 computed(多層)
    } else if (sub.kind === "effect") {
      Effects.get(sub)?.schedule(); // 讓 effect 排進 microtask
    }
  }
}
export function computed<T>(
  fn: () => T,
  equals: Comparator<T> = defaultEquals
) {
  const node: Node & {
    kind: "computed";
    value: T;
    stale: boolean;
    equals: Comparator<T>;
    computing: boolean;
    hasValue: boolean;
  } = {
    kind: "computed",
    deps: new Set(),
    subs: new Set(),
    value: undefined as unknown as T,
    stale: true, // 第一次讀時要算
    equals,
    computing: false,
    hasValue: false,
  };
  function recompute() {
    if (node.computing) throw new Error("Cycle detected in computed");
    node.computing = true;
    // 解除舊依賴(避免依賴漂移造成洩漏)
    for (const d of [...node.deps]) unlink(node, d);
    // 在追蹤上下文中計算,過程中讀到誰就自動 link(node → dep)
    const next = withObserver(node, fn);
    if (!node.hasValue || !node.equals(node.value, next)) {
      node.value = next;
      node.hasValue = true;
    }
    node.stale = false;
    node.computing = false;
  }
  const get = () => {
    track(node); // 讓觀察者(effect / computed)能訂閱我
    if (node.stale || !node.hasValue) recompute();
    return node.value;
  };
  const peek = () => node.value;
  const dispose = () => {
    // 解除與所有上/下游關係
    for (const d of [...node.deps]) unlink(node, d);
    for (const s of [...node.subs]) unlink(s, node);
    node.deps.clear();
    node.subs.clear();
    node.stale = true;
    node.hasValue = false;
  };
  // peek, dispose, _node 都是為了測試方便,正常情況下 computed 只要回傳 get 的方法就好
  return { get, peek, dispose, _node: node };
}
computed 接進 signal把 signal.ts 的 set() 訂閱迴圈補上 computed 分支:
// signal.ts
import { markStale } from "./computed";
// ...位於 set() 段落
for (const sub of node.subs) {
  if (sub.kind === "effect") {
    Effects.get(sub)?.schedule();
  } else if (sub.kind === "computed") {
    markStale(sub); // 只標髒,不立刻重算
  }
}
push 失效(標髒標記) + pull 計算(讀取時才計算),effect 會被排程重跑,但他在回呼的 compute.get() 處才會被觸發真正的重算。
const a = signal(1);
const b = signal(2);
const sum = computed(() => a.get() + b.get()); // 3
const double = computed(() => sum.get() * 2); // 6
const stop = createEffect(() => {
  console.log("double =", double.get());
  onCleanup(() => console.log("cleanup")); // 重跑前呼叫
});
a.set(5); // 標髒標記 sum、double;排程 effect
// microtask:cleanup → "double = 14"(此刻才重算 sum 與 double)
stop();

到目前為止我們完成了:
signal.set():push 失效——對 computed 只標記 stale,對 effect 交給排程(microtask 合併)Node → EffectInstance 的查表(Symbol/WeakMap 皆可)下一篇,我們來處理 Batch & Transation 的機制,來解決不必要的多次更新問題。