上一篇,我們介紹了:
這一篇我們透過一些範例來理解錯誤與解決辦法。
症狀: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。