有了第一篇的概念之後,相信大家都已經在心中埋下懷疑的種子,那我們來重新審視一下 React 是怎麼處理 state 的吧!
相信大家對這套流程並不陌生吧! 可以說是面試必考問題,詳細的可以參考官方文檔。
那我們接著看看 Fine-grained Reactivity 的概念下是怎麼處裡的:
下面用個表格來比較一下差異會比較直觀一點:
面向 | React useState & VDOM |
Fine-Grained Reactivity (Signal/Atom) |
---|---|---|
更新單位 | Component(整棵子樹) | State cell(單一值) |
依賴追蹤方式 | 每次 render 時重新執行函式 → 比對 VDOM | 讀取時即註冊依賴,寫入時精準通知 |
排程模型 | 異步批次 (setState → 任務列) + Diff + Commit |
異步批次 (batch) + 拓撲排序 + 直接執行副作用 |
閒置成本 | 即使視覺不變仍需重跑函式、生成 VDOM | 若結果相同會短路,不進入下游 |
心智模型 | 「宣告 UI = f(state)」 | 「宣告 state = f(source);UI 只是其中一種 Effect」 |
典型最佳化手段 | memo , useCallback , useMemo |
內建;僅在需要跨邏輯分層時才顯式 memo |
DevTools 生態 | 成熟、豐富 | 正在起步(Solid, Vue3, MobX DevTools…) |
Signal 讓「資料 ≈ 最小可觀測單元」,將重渲染邊界從 Component 級縮到 State 級,系統複雜度隨應用規模成 線性 增長,而非 指數 增長。
術語 | 對應 React 類比 | 功能 |
---|---|---|
Source / Signal | useState 的 state 變數 |
最原始、可寫入的資料節點 |
Computed / Derivation | useMemo |
由 Signal 衍生、具快取的純函數 |
Effect / Reaction | useEffect |
為了 副作用,在追蹤到依賴變化後觸發 |
Batch / Transaction | unstable_batchedUpdates |
將多次寫入壓縮為一次更新傳播 |
Graph / Dependency Map | React Fiber | 追蹤資料依賴關係的有向圖 |
當
Computed C
依賴Source A
與Source B
;當任一 Source 改變,只重算 C,隨後觸發Effect D
。沒有任何涉入的元件被渲染,除非 D 裡真的去動到 DOM。
以下以 Solid.js / Vue3 Composition API 為代表;MobX、Jotai、Signals.js 亦同理。
階段 | 示意程式碼 | 內部動作 |
---|---|---|
讀 (Tracking) | console.log(count()) |
Runtime 記錄「目前執行的 Computed/Effect 依賴 count 」 |
寫 (Mark Dirty) | count.set(v => v + 1) |
只做兩件事: 1. 更新值; 2. 把受影響節點標為 dirty ,放進 queue |
傳 (Propagate) | (Scheduler flush) | 拓撲排序:先重算 Computed → 若值變,才往下游 Effect;否則終止 |
結果:若 count +1
後,某個 Computed 回傳值仍相同(e.g. Math.floor),整條支鏈都不會觸發,成本≈受影響節點數 × O(1)。
function Counter() {
const [count, set] = useState(0);
return <button onClick={() => set(c => c + 1)}><snap>{count}</snap></button>;
}
Counter
重新執行 → 產生新 VDOM
→ diff <button>
→ commit;Counter
包在父層巨型表格內,整行 React call stack 依序出現。const [count, setCount] = createSignal(0);
const Counter = () => (
<button onClick={() => setCount(c => c + 1)}>
<snap>{count()}</snap> {/* 文字節點直接綁定 signal */}
</button>
);
只重算受影響的 Computed / Effect
;大型列表、White-board、Spreadsheets 成本與資料單元數線性相關。
依賴圖是顯式可走訪的有向圖 → 可做變動追蹤、諸如 why-did-you-update
之類的分析,而不用把 Fiber 打平成 JSON 再逆向追。
effect(() => doSomething(domRef(), data()))
只會在依賴值真正改變時觸發,避免 React useEffect
常見的「deps array 漏 / 過度依賴」陷阱。
Computed
是純函式;可在 Node 中跑邏輯測試,不必拉 DOM/mock React Test Renderer
。
useSyncExternalStore
包起來。問題 | 回應 |
---|---|
和 MobX/Proxy-based 寫法差在哪? | Signal 強制使用 getter/setter 暴露變數 (signal.value );可預測、不依賴 Proxy 差異化行為。 |
支援 Async 嗎? | 核心只處理同步值 → 高階 API (createResource /createAsync ) 以狀態機包裹 Promise ;更新仍走相同依賴圖。 |
需要 Context 嗎? | 跨階層共享狀態可直接傳遞 Signal;理論上不需 Context Provider,除非要借用 React DevTools 等生態系統。 |
效能是否一定更好? | 在 高互動 & 大量細粒度更新 場景明顯領先;但對 靜態內容或批量一次性渲染,React diff 成本可忽略。選型仍視需求而定。 |
Fine-grained reactivity 的本質優勢,在於把「重新計算」縮小到資料最小單元,讓效能與複雜度不再被元件樹綑綁。
Signal 透過顯式資料單元、隱式依賴追蹤、最小傳播結構,把 UI 更新問題轉譯成「圖論 + 排程」,這就是從程式設計角度切入前端效能的現代解法。
下一篇,我們來深入理解 Fine-grained reactivity 的思想與發展過程。