在現代前端開發中,「狀態(State)」是驅動 UI 變更的核心。當狀態改變時,框架如何得知?又是如何決定哪一部份的 DOM 需要更新?
React 的 useState 採用了 不可變資料(Immutability)與重繪(Re-render) 的哲学;而 Vue 3 與 Angular 則走出了完全不同的技術路線。
本文將從 狀態更新機制、記憶體追蹤(Memory Tracking)與 UI 變更偵測(Change Detection) 的角度,深入解析三者的實踐與底層邏輯。
要理解 React 的 useState,必須掌握兩個關鍵概念:不可變資料(Immutable Data) 與 快照(Snapshot)。
在 React 中,狀態變更不會直接修改原本的變數,而是透過useState解構出來的 Setter 函式(即範例中的 setCount)傳入一個「全新」的值,藉此通知 React 觸發 Re-render。
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
// ❌ 直接修改無效:count = count + 1;
// ✅ 必須透過 Setter 觸發 Re-render
setCount(count + 1);
};
return <button onClick={handleClick}>Count: {count}</button>;
}
count 都是一個獨立的 常數(Primitive Value/Snapshot)。呼叫 setCount 並不會「原地修改」count 的記憶體內容,而是告訴 React:「請用新的狀態值,重新執行一次 Counter() 函式」。與 React 要求 Immutable 不同,Vue 3 的核心哲學是 可變資料(Mutable Data)與響應式依賴追蹤(Reactivity System)。
Vue 3 在 <script setup> 中定義狀態,會使用 ref() 或 reactive()。
<script setup>
import { ref } from 'vue'
// 1. 建立一個響應式物件
const count = ref(0)
const handleClick = () => {
// ✅ 直接修改 .value 屬性(Mutable Write)
count.value++
}
</script>
<template>
<button @click="handleClick">Count: {{ count }}</button>
</template>
ref(0) 在 Heap 記憶體中建立了一個含有 .value getter/setter 的包裝物件(內部透過 Proxy 或 Property Accessor 實作)。<script setup> 區塊只會執行一次!count.value 時,Vue 的響應式系統自動將該 DOM 節點記錄為「依賴者(Subscriber)」。count.value++ 時,觸發 Setter,Vue 精準地更新與該狀態綁定的 DOM,不需要進行整個組件樹的 Virtual DOM 大面積比對。Angular 在狀態管理上經歷了重大革新。傳統上依賴 Zone.js + 髒檢查(Dirty Checking),而現代 Angular (v16+) 則引進了 Signals 邁向細粒度更新。
Signals 是 Angular 迎來細粒度響應式的核心 API,語法與 Vue 的 ref 有異曲同工之妙,但更為嚴謹。
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-counter',
template: `<button (click)="handleClick()">Count: {{ count() }}</button>`
})
export class CounterComponent {
// 1. 定義一個 WritableSignal
count = signal(0);
handleClick() {
// 2. 更新狀態(使用 .update() 或 .set())
this.count.update(val => val + 1);
}
}
signal() 建立了一個可被訂閱的 Getter 函式。讀取時呼叫 count(),更新時使用 .set() 或 .update()。在沒有 Signals 的傳統 Angular 中,你甚至不需要呼叫任何特殊的 Hook 或 API,直接修改 Class 屬性即可:
export class CounterComponent {
count = 0; // 普通 Class 屬性
handleClick() {
this.count++; // 直接修改!Zone.js 會攔截非步事件並觸發全組件樹的 Change Detection
}
}
| 比較維度 | React (useState) |
Vue 3 (ref / reactive) |
Angular (Signals / 傳統) |
|---|---|---|---|
| 資料哲學 | 不可變(Immutable) | 可變(Mutable Proxy) | 可變(Signal 包裝 / 普通 Class 屬性) |
| 語法特徵 | const [val, setVal] = useState() |
const val = ref() / val.value++ |
val = signal() / val.update() |
| 狀態變更方式 | 呼叫 setVal(newVal) |
直接給值 val.value = newVal |
呼叫 val.set() 或 val.update() |
| 更新機制 | 重新執行整個組件函式,比對 Virtual DOM | 依賴收集(Proxy),精準更新受影響 DOM | Signals:細粒度局部更新 |
傳統:Zone.js 觸發 Change Detection |
| 閉包陷阱(Stale Closure) | ⚠️ 極易發生(若未處理好依賴陣列) | ❌ 無(setup 僅執行一次,變數指標不變) | ❌ 無(Class 實體屬性天然綁定 this) |
useEffect 或非步 callback(如 setTimeout)中使用了舊 Render 的狀態,就會讀取到舊的快照(Stale State)。ref 物件指標固定,Angular 的 Class this 實體固定,非同步寫法中永遠能讀取到最新的 ref.value 或 this.prop,徹底避開了閉包陷阱。React.memo、useMemo、useCallback 來切斷不必要的 Render Tree。useState** 的本質是「單向資料流與不可變性」,以重繪函式換取預測性強、易於追蹤的快照狀態。ref** 的本質是「透明的響應式代理」,透過 Proxy 自動追蹤依賴,給予開發者直覺的原地修改體驗。理解這三種狀態管理的底層機制,不僅能幫你在框架切換時少走彎路,更能讓你在面對效能瓶頸時,一眼看穿問題出在重繪(Re-render)還是變更偵測(Change Detection)上!