上一篇,我們介紹了:
這一篇我們透過一些範例來理解錯誤與解決辦法。
症狀:setTimeout / debounce / Promise.then
內用到的值總是舊的。
原因:事件處理/非同步回呼捕獲的是渲染當下的快照。
修正:回呼當下再次取快照(peek()
),或以函式取值避免閉包持久化。
// ❌ 之後的 onClick / timeout 一直用「那次 render 的 v」
const v = useSignalValue(countSig);
const onClick = () => setTimeout(() => console.log(v), 500);
// ✅ 回呼當下再取最新值(不追蹤)
const onClick2 = () => setTimeout(() => console.log(countSig.peek()), 500);
// ✅ 或注入取值函式
const getCount = () => countSig.peek();
const onClick3 = () => setTimeout(() => console.log(getCount()), 500);
症狀:每次 render 都重建實例,導致訂閱洩漏或效能抖動。
原因:元件函式每 render 都執行;沒有用 useMemo
/自建 hook 穩定化。
修正:用 useSignalState
/useComputed
或 useMemo
只建一次。
// ❌ 每次 render 都 new,一定洩漏
function Bad() {
const local = signal(0);
const sum = computed(() => local.get() + 1);
// ...
}
// ✅ 穩定實例
function Good() {
const [local, setLocal] = useSignalState(0);
const sum = useComputed(() => local + 1);
}
computed
依賴 React 快照(不會更新)症狀:useComputed(() => count * 2)
僅第一次生效,之後不變。
原因:computed
只會追蹤 在 callback 內呼叫 .get()
的 signal;React 的數值快照不在 reactive graph。
修正:在 computed
內讀 signal.get()
;若只是渲染期快取,用 useMemo
。
// ❌ 不追蹤
const count = useSignalValue(countSig);
const doubled1 = useComputed(() => count * 2);
// ✅ 追蹤 signal
const doubled2 = useComputed(() => countSig.get() * 2);
// ✅ 只要渲染用快取
const doubled3 = React.useMemo(() => count * 2, [count]);
症狀:Concurrent 模式下 UI 與資料不同步、或 render 次數異常多。
原因:自己在 useEffect
裡 createEffect
然後 setState
,React 無法在 commit 前重取快照。
修正:一律使用 useSignalValue / useSignalSelector
(內部走 useSyncExternalStore
)。
// ❌ 容易撕裂
useEffect(() => {
const stop = createEffect(() => setV(src.peek()));
return () => stop();
}, []);
// ✅ tear-free
const v = useSignalValue(src);
.get()
重新建立依賴症狀:清理(cleanup)期間不小心又建立了新的依賴邊,造成異味或洩漏。
原因:在 onCleanup
/ React cleanup 內呼叫 .get()
。
修正:用先前的快照或 peek()
;避免在清理期追蹤。
createEffect(() => {
const last = someComputed.peek(); // 先取快照
onCleanup(() => {
// ❌ avoid: someComputed.get();
last; // ✅ 用快照
someComputed.peek(); // ✅ 或:peek 不追蹤
});
});
症狀:同一區塊,一些元素先變、一 些晚變。
原因:有的讀 signals(即刻反映),有的走 React setState + Transition(延後)。
修正:統一來源。
// ✅ 統一讀外部,顯示端用 deferred
const q = useSignalValue(querySig);
const dq = useDeferredValue(q);
// ✅ 先用 React state 緩衝,再 commit 到 signal
const [draft, setDraft] = useState(useSignalValue(titleSig));
startTransition(() => titleSig.set(draft));
useSignalValue
/ useSignalSelector
。computed
callback 內一定要是 signal.get()
。useSignalState
/ useComputed
穩定化。peek()
,避免 .get()
。useEffect
);資料副作用交給 our effect(createEffect
)。有了以上的範例,能更加讓大家掌握住使用 Signal 的特點,主要是幫忙避坑啦!
到這裡,我們已完成一套可用的核心與 React 實戰:
signal / effect / computed
(push 標髒標記 + pull 重算),最小 scheduler(microtask 合併)。useSyncExternalStore
橋接的 useSignalValue
/ useComputed
/ useSignalSelector
,以及在 Concurrent/StrictMode 下的副作用分工與清理時機。接下來,換個角度讓我們把同一套心智帶進 Vue。好消息是:本系列的 computed
與 Vue 的 computed
幾乎等價,實作上會非常順。
我們會做兩個極簡 hook,讓模板直接使用你的狀態與衍生值:
// vue-adapter
export function useSignalRef<T>(src: { get(): T; peek(): T }) { /* 映射到 Vue ref,onUnmounted 清理 */ }
export function useComputedRef<T>(fn: () => T, equals = Object.is) { /* 生命週期內建立 computed,並轉成 ref */ }
useComputedRef
的 callback 要讀 signal.get()
,不是讀 ref.value
;否則只會變成純 Vue 計算,失去你的 reactive graph。ref
,不把 Vue 的 reactivity 反向接回你的圖,避免雙重依賴造成排程繞圈。onUnmounted
:卸載即釋放 createEffect
/computed
。