跳至主要内容

什麼是結構共享(Structural Sharing)?為什麼淺拷貝搭配結構共享既安全又有效率?

如果你寫過 React、Redux 或用過 Immer、React Query,你其實早就天天在使用「結構共享」了,只是可能沒有意識到。這篇文章會用最淺白的方式,帶你搞懂這個概念。


一、先從「不可變資料(Immutable Data)」說起

在 React 的世界裡,有一條鐵律:不要直接修改 state,而是要產生一個新的物件

// ❌ 錯誤:直接修改
state.user.name = '小明';

// ✅ 正確:產生新物件
setState({ ...state, user: { ...state.user, name: '小明' } });

為什麼要這麼麻煩?因為 React 判斷「資料有沒有變」的方式非常簡單粗暴——比較記憶體位址(reference),也就是 oldState === newState。如果你直接改舊物件,位址沒變,React 會以為「什麼都沒發生」,畫面就不會更新。

但這裡出現一個疑問:

「每次都要複製一份新的資料,如果資料很大,不會很浪費效能和記憶體嗎?」

答案是:不會,因為我們用的是淺拷貝 + 結構共享


二、什麼是結構共享(Structural Sharing)?

用搬家來比喻

想像你的 state 是一棟三層樓的房子:

房子(state)
├── 一樓:user資料
├── 二樓:todos清單
└── 三樓:settings設定

現在你只想改「一樓的一張沙發」。你會怎麼做?

  • 深拷貝(Deep Copy)的做法:把整棟房子連同每一層的每一件家具,全部重新打造一遍,再換掉那張沙發。→ 超級浪費!
  • 結構共享的做法:蓋一個新的「房子外殼」,一樓重新裝潢(換沙發),但二樓和三樓直接沿用原本的,連動都不用動。

用程式碼表達就是:

const oldState = {
user: { name: '小華', age: 25 },
todos: [ /* 一萬筆待辦事項 */ ],
settings: { theme: 'dark' },
};

// 只改 user.name
const newState = {
...oldState, // 蓋新的外殼
user: { ...oldState.user, name: '小明' }, // 一樓重新裝潢
};

// 見證奇蹟的時刻:
console.log(newState === oldState); // false → 外殼是新的
console.log(newState.user === oldState.user); // false → user 是新的
console.log(newState.todos === oldState.todos); // true → 共享!
console.log(newState.settings === oldState.settings); // true → 共享!

那一萬筆的 todos 完全沒有被複製,新舊 state 指向的是同一份 todos。這就是結構共享:

只複製「有改動的那條路徑」,沒改動的部分,新舊資料直接共用同一份。

圖解

   oldState                    newState
│ │
├── user(舊) ├── user(新)← 只有這條路徑是新的
│ │
├── todos ────┐ ┌──────── todos
│ ▼ ▼
│ 【同一份資料】← 共享,零複製成本

├── settings ─┐ ┌──────── settings
▼ ▼
【同一份資料】← 共享

三、什麼是淺拷貝(Shallow Copy)?

淺拷貝就是「只複製最外面那一層,裡面的東西照舊引用」。

const original = {
name: '小華',
hobbies: ['爬山', '游泳'],
};

const copy = { ...original }; // 展開運算子就是淺拷貝

console.log(copy === original); // false → 外層是新物件
console.log(copy.hobbies === original.hobbies); // true → 內層還是同一份

常見的淺拷貝方式:

寫法適用對象
{ ...obj }物件
[ ...arr ]陣列
Object.assign({}, obj)物件
arr.slice()陣列

你有沒有發現?結構共享,其實就是「一層一層地做淺拷貝」做出來的。每往下改一層,就對那一層淺拷貝一次;沒改到的分支,自然就被共享了。


四、為什麼「淺拷貝 + 結構共享」是安全的?

你可能會擔心:「新舊 state 共用同一份 todos,不會互相污染嗎?」

關鍵在於一個約定:不可變性(Immutability)——我們永遠不直接修改既有的物件

只要遵守這條規則:

  1. 舊 state 裡的 todos 永遠不會被改動(要改就產生新的)。
  2. 所以新 state 引用它,是 100% 安全的——它是「唯讀」的,不會有人偷改。
  3. 任何人拿到舊 state,看到的永遠是當初的樣子(這也讓「時光旅行除錯」、「undo/redo」變得超簡單)。

換句話說:

共享之所以安全,是因為被共享的東西永遠不會被修改。 危險的從來不是共享本身,而是「共享之後還去改它」。

反過來說,如果你沒有遵守不可變性(例如 state.todos.push(newItem)),那共享就會變成災難——新舊 state 會一起被改壞,而且 React 還偵測不到變化。


五、為什麼是有效率的?

1. 寫入快:只複製一條路徑

假設 state 是一棵有 10,000 個節點的樹,你只改其中一個葉子:

  • 深拷貝:複製 10,000 個節點 → O(n)
  • 結構共享:只複製從根到那個葉子「沿路經過」的節點,可能只有 3~4 個 → 幾乎 O(路徑深度)

2. 比較快:一行 === 就搞定

這是結構共享最大的殺手級應用。React 的 memouseMemo、Redux 的 useSelector 都靠它:

// 想知道 todos 有沒有變?
if (newState.todos === oldState.todos) {
// 位址相同 → 保證沒變 → 這個元件不用重新 render!
}

不需要一筆一筆比對一萬筆待辦事項的內容(深度比較很貴),只要比較一次記憶體位址(奈秒等級)就能得到答案。這就是為什麼 React 元件可以精準地「只重繪有變動的部分」。

3. 省記憶體

沒被改到的資料只存在一份,一百個歷史版本的 state 可能 95% 的內容都是共用的。Redux DevTools 能記錄幾百步的 state 歷史還不爆記憶體,就是這個原因。


六、實際範例:更新巢狀資料

假設有這樣的 state,我們要把 id: 2 的待辦事項打勾:

const state = {
filter: 'all',
todos: [
{ id: 1, text: '買菜', done: false },
{ id: 2, text: '寫程式', done: false },
{ id: 3, text: '運動', done: false },
],
};

// 手動實現結構共享
const newState = {
...state, // 外層淺拷貝(filter 被共享)
todos: state.todos.map(todo => // 陣列產生新的
todo.id === 2
? { ...todo, done: true } // 只有 id:2 產生新物件
: todo // 其他 todo 直接沿用(共享!)
),
};

console.log(newState.todos[0] === state.todos[0]); // true → 共享
console.log(newState.todos[1] === state.todos[1]); // false → 新的
console.log(newState.todos[2] === state.todos[2]); // true → 共享

如果畫面上每個 todo 都是一個用 React.memo 包起來的元件,那麼只有 id:2 的那個元件會重新 render,其他兩個因為 props 位址沒變,直接跳過。效能優化就這樣自然發生了。


七、覺得手寫展開運算子很累?交給工具

巢狀一深,手寫 ... 會變得很痛苦。實務上常用這些工具,它們底層都是在幫你做結構共享:

  • Immer(Redux Toolkit 內建):讓你用「直接修改」的語法,底層自動產生結構共享的新物件。
import { produce } from 'immer';

const newState = produce(state, draft => {
draft.todos[1].done = true; // 看起來像直接改,其實很安全
});
  • React Query / TanStack Query:重新抓取資料後,會自動比對新舊回應,內容沒變的部分沿用舊引用,避免元件無謂地重新 render。
  • Immutable.js:用特殊資料結構(如 HAMT)把結構共享做到極致,不過學習成本較高,現在較少新專案採用。

八、重點整理

概念一句話說明
淺拷貝只複製最外層,內層沿用原本的引用
結構共享只重建「有改動的路徑」,其餘分支新舊共用
為什麼安全因為搭配不可變性——被共享的資料永遠不會被修改
為什麼高效寫入只複製一條路徑、變更偵測只需 ===、記憶體大量共用

最後記住這個心法:

深拷貝是「全部重做」,結構共享是「只換有動到的,其他借來用」——而借來用之所以不會出事,是因為大家說好了誰都不准改。