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 的機制,來解決不必要的多次更新問題。