《React 思維進化》 筆記系列
1. [React] DOM, Virtual DOM 與 React element
2. [React] 了解 JSX 與其語法、畫面渲染技巧
3. [React] 單向資料流介紹以及 component 初探
4. [React] 認識狀態管理機制 state 與畫面更新機制 reconciliation
5. [React] 在子 component 觸發父 component 資料更新、深入理解 batch update 與 updater function
6. [React] 了解 immutable state 與 immutable update 方法
7. [React] 認識 component 的生命週期、了解每次 render 都有自己版本的資料
8. [React] React 中的副作用處理、初探 useEffect
9. [React] 對 hooks 的 dependencies 誠實,以維護資料流的連動
10. [React] React 18 effect 函式執行兩次的原因及 useEffect 常見情境
11. [React] 認識 useCallback、useMemo,了解 hooks 運作原理

前言#
承接上篇 [React] 在子 component 觸發父 component 資料更新、深入理解 batch update 與 updater function,此篇主要敘述的是《React 思維進化》 3–3 ~ 3–4 章節的筆記,若有錯誤歡迎大家回覆告訴我~
什麼是 immutable、什麼是 mutate#
在 React 的 state 資料中,我們可儲存任何型別的資料,但如果是存放物件或陣列型別的資料,想要更新時,應該根據更新需求重新產生新的物件或陣列,為什麼呢~?接下來就先來看看什麼是 immutable、什麼是 mutate吧!
在 JavaScript 中,原始型別(primitive)的資料是 immutable (不可變的) 的,所謂 immutable 是指這些值本身不能被修改,若希望更新資料,只能「產生一個新值來取代舊的」。
補充:JavaScript 的原始型別
- 包含:String(字串)、Number(數字)、Boolean(布林值)、Undefined(未定義)、Null(空值)、Symbol(符號)、BigInt(大整數)
- 儲存的是「值」本身
- 原始型別的值在賦予給其他變數時,會複製值,而非複製參考
- 賦值給原始型別變數時,變數原有的值不會被修改,而是被新的值取代
舉例而言,當我們嘗試修改字串值內容時,字串不會有任何反應或改變:

如果我們想要更新字串資料,要重新產生新字串來取代舊字串,才能順利更新:

其中,str = 'Heact'; 是對 str 這變數「重新賦值」,改的是 str 這變數要指向哪個字串,而非改變字串 'React' 這值本身的內容,示意圖如下:

同樣的,在 React 更新原始型別的 state 資料,一樣會產生並指定新值來取代舊值:
const [number, setNumber] = useState(0);const [name, setName] = useState('React');const [isActive, setIsActive] = useState(false);
setNumber(1); // 產生一個全新的數字值 1,並傳給 setNumber 來取代舊的值 0,原本的值 0 本身沒有被修改setName('Foo'); // 產生一個全新的字串值 'Foo',並傳給 setName 來取代舊的值 'React',原本的值 'React' 本身沒有被修改setIsActive(true); // 產生一個全新的布林值 true,並傳給 setIsActive 來取代舊的值 true,原本的值 false 本身沒有被修改物件與陣列#
相對於上述的原始型別,在 JavaScript 中,物件與陣列是以「參考(reference)」形式存在的資料,物件或陣列本身的內容是可變(mutable)的,修改其屬性或項目內容的操作稱為「mutate」。
const position = {x: 0, y: 0};const names = ['HTML', 'CSS', 'JavaScript'];
position.x = 10; //mutate position 的屬性names[0] = 'React'; //mutate names 的項目mutate 物件或陣列的屬性內容時,此變數的參考對象不會改變,只是內容被修改,示意圖如下:


如果要改變變數的參考、或避免改變舊有物件或陣列內容,應產生全新的物件或陣列來取代舊的。
(補充:關於 JavaScript 的傳值或傳址,可參考這篇:你不可不知的 JavaScript 二三事#Day26:程式界的哈姆雷特 — — Pass by value, or Pass by reference?)
保持 state 的 immutable#
在 React,不該 mutate 一個物件或陣列型別的 state 資料,而是應該產生新物件或陣列來取代舊的,這種「資料一旦被建立後就不會再被事後修改」就稱為「immutable(不可變的)」。
保持 state 資料 immutable 是 React 的重要守則,在 JavaScript 中,物件與陣列都是 mutable 的,若要以 immutable 的方式操作物件或陣列,需要開發者自己建立安全手段來操作資料,自行維持 immutable state 的原則。
為何需要保持 state 的 immutable ?因為 React element 代表某個歷史時刻的畫面結構,一經建立後就不該被修改,而 state 作為原始資料同理,是用來表示 component 某個歷史時刻的資料,一旦被建立後就不該被修改,以維持單向資料流可靠性。
那如果不維持 state 的 immutable 會怎麼樣呢?接著來看看不維持 state 的 immutable 可能會產生的問題~
不維持 state 的 immutable 會怎麼樣? 👽 呼叫 setState 方法時的新舊資料檢查需求#
如果我們有一個 state 以物件形式儲存文章的基本資料,期待在點擊按鈕後觸發文章資料的更新,並呼叫 re-render 來更新畫面。
以下是錯誤嘗試 1:以 mutate 方式修改物件資料,在點擊按鈕後,不會觸發畫面更新:
import { useState } from "react";
export default function App() { const [articleInfo, setArticleInfo] = useState({ title: "My title", description: "this is my article", category: "React", });
const handleClick = () => { //⛔️ 以下是錯誤的 state 更新方式 articleInfo.title = "My NEW title";
};
return ( <div> <div>title: {articleInfo.title}</div> <div>description: {articleInfo.description}</div> <div>category: {articleInfo.category}</div> <button onClick={handleClick}>click me</button> </div> );}讀者可能想說,這很正常呀,因為沒有呼叫 setArticleInfo 來觸發 re-render,畫面當然不會更新。
因此有了錯誤嘗試 2:加上 seArticleInfo 的呼叫,仍無法觸發畫面更新:
import { useState } from "react";
export default function App() { const [articleInfo, setArticleInfo] = useState({ title: "My title", description: "this is my article", category: "React", });
const handleClick = () => { //⛔️ 以下是錯誤的 state 更新方式 //因為 articleInfo 存的參考的值仍相同,透過 Object.is() 比較後發現相同,因此不會觸發 re-render articleInfo.title = "My NEW title"; setArticleInfo(articleInfo); };
return ( <div> <div>title: {articleInfo.title}</div> <div>description: {articleInfo.description}</div> <div>category: {articleInfo.category}</div> <button onClick={handleClick}>click me</button> </div> );}為何不會更新?我們之前提過,reconciliation 過程中,當 setState 被呼叫後,React 會先以 Object.is() 來檢查新舊 state 值是否相同,若相同則判定資料沒更新,會直接中斷後續流程。而因為 articleInfo.title = “My NEW titile”; 是 mutate 既有物件, articleInfo 本身存的參考沒變,呼叫 setArticleInfo(articleInfo); 傳入的 articleInfo 還是舊的物件參考,因此 Object.is() 比較後會發現新舊值都指向同一個參考,判定相同、因而中斷後續流程。
那要如何讓畫面如預期更新?要在呼叫 setArticleInfo 時傳入新產生的物件:
import { useState } from "react";
export default function App() { const [articleInfo, setArticleInfo] = useState({ title: "My title", description: "this is my article", category: "React", });
const handleClick = () => { //✅ 建立新物件作為新 state 資料,沒有修改既有 articleInfo 物件 //newArticleInfo 會得到新的參考位址,和既有的 articleInfo 參考不同,因此會觸發 re-render const newArticleInfo = { title: "My NEW title", description: "this is my article", category: "React", }; setArticleInfo(newArticleInfo); };
return ( <div> <div>title: {articleInfo.title}</div> <div>description: {articleInfo.description}</div> <div>category: {articleInfo.category}</div> <button onClick={handleClick}>click me</button> </div> );}當 setState 方法被呼叫後,React 會嘗試判定一個物件或陣列 state 是否改變,判定方式是看既有資料和新資料的「**參考」**是否相同,而不會檢查物件或陣列內的資料內容是否不同;因此即使傳給 setState 與原資料內容完全相同、但參考不同的物件或陣列,React 仍判定資料有更新,會繼續 re-render。
因此,不維持 state 的 immutable 會怎麼樣?會導致呼叫 setState 方法時,React 無法透過資料參考是否相同來判定新舊值是否相同,進而導致setState方法無法如我們預期的觸發 re-render、導致資料與畫面不一致。
不維持 state 的 immutable 會怎麼樣? 👽 過去 render 的舊 state 仍有被讀取的需求#
在應用程式的商業邏輯中,我們可能需要讀取過去 render 的舊 state 資料,如果 mutate 了舊 state,可能會丟失資料的歷史紀錄、進而導致後續的邏輯失效。
以一個非同步事件的程式碼為例:
import { useState } from 'react';
export default function App() { const [player, setPlayer] = useState({ position: { x: 0, y: 0 } });
const moveToRight = () => { //⛔️ 以下是錯誤的 state 更新方式 player.position.x += 1; setPlayer({ ...player }); };
const alertCurrentPosition = () => { setTimeout( () => { alert(`x: ${player.position.x}, y: ${player.position.y}`) }, 3000 ); } return ( <div> x: {player.position.x}, y: {player.position.y} <div> <button onClick={moveToRight}>move to right</button> <button onClick={alertCurrentPosition}> alert current position after 3 secs </button> </div> </div> );}在這範例中有兩個按鈕,各自綁定了事件處理,我們預期在點擊 「move to right」按鈕後, x 值能增加 1,而 y 不變;也預期在點擊 「alert current position after 3 secs」按鈕後,3 秒後會 alert 顯示點擊此按鈕瞬間的 position 資訊。
舉例來說,當位置為 0, 0 時,我們預期點擊 alert 按鈕後,會固定跳出 x: 0, y: 0 的資訊,但如果我們在位置為 0, 0 時,點擊 alert 按鈕,接著快速的在三秒內點 “move to right” 移動位置,最後跳出的 alert 會是x: 3, y: 0,而非預期結果x: 0, y: 0。示意動畫如下:

為何會有這個非預期結果?因為我們在 player.position.x += 1; 這裡錯誤 mutate 既有物件內容,導致每次更新 state 時,也一併修改了過去 render 的舊版 state 資料,因此 alertCurrentPosition 事件取出的舊版本 position資料已被修改過,導致最後出來的結果不如預期。
在非同步事件中,事件可能在 component 已 re-render 後才去讀舊 render 的 state,而因為我們用 mutate 的方式修改舊有資料,導致拿到的歷史資料已被修改,進而導致商業邏輯出錯。
除了非同步事件外,其他需要讀取舊 render 資料來進行後續邏輯處理的情境如:文章編輯的 undo / redo 功能、訂單狀態由 A 變為 B 後要進行特定處理…等,如果我們竄改了舊 state 資料,就會導致依賴這些資料的邏輯無法運作。
補充:非同步事件只是一種 mutate state 可能會有問題的情境,不是判斷要不要 immutable update 的依據,無論有沒有在非同步事件讀取 state,在所有情境都應該保持 state immutable。
因此,不維持 state 的 immutable 會怎麼樣?會導致需要讀取舊 render 資料來進行判斷或處理的商業邏輯出錯,導致應用程式的行為不如預期。
不維持 state 的 immutable 會怎麼樣?👽 React 效能優化機制的參考檢查需求#
React 很多效能優化機制會以資料的參考是否相同作為判斷依據,機制如:
useEffectuseCallbackuseMemoReact.memo
若直接修改既有物件或陣列 state 資料,這些機制可能不會意識到資料有變化(因為內容改變,但參考沒變),機制運作會產生異常。
因此,不維持 state 的 immutable 會怎麼樣?會導致 React 的效能優化機制運作異常,導致某些時刻應用出現非預期錯誤。
💡小結,以 React 開發時,開發者要謹記,要維持資料處於「只要參考沒變就代表內容沒變,只要參考有變就代表內容有變」 的狀態。
Immutable update#
說了這麼多要以 immutable 的方式更新 state 資料,那實務上我們該用什麼方式來 immutable 更新物件或陣列資料呢?接下來要介紹如何以 immutable 的方式更新物件或陣列資料,immutable update 不限於 React 的 state 情境,所以我們先從純 JavaScript 來看~
物件資料的 immutable update 方法#
以 spread 語法來複製物件的內容,並加上新屬性或更新既有屬性#
以 immutable 方式新增或修改物件的屬性,可分為兩步驟:
- 建立一個全新物件,把既有物件的全部屬性複製到新物件中
- 在新物件加上新屬性,或覆蓋想修改的屬性的值
而我們可以用 JavaScript ES6 的 spread 語法來複製物件所有屬性到另一個物件,並在後面加上想要覆蓋或新增的屬性:
const oldObj = {a: 10, b: 20, c: 30};const newObj = {...oldObj, a: 100, d: 400};
console.log(oldObj); //{a: 10, b: 20, c: 30}console.log(newObj); //{a: 100, b: 20, c: 30, d: 400}而如果遇到巢狀物件結構,則需要在有涉及屬性更新的每一層物件都做對應的 spread 屬性複製:
const oldObj = { a: 1, b: 2, innerObj1: {c: 3, d: 4}, innerObj2: {e: 5}};
const newObj = { ...oldObj, innerObj1: {...oldObj.innerObj1, d: 100}}
console.log(oldObj);//{a: 1, b: 2, innerObj1: {c: 3, d: 4}, innerObj2: {e: 5}}
console.log(newObj);//{a: 1, b: 2, innerObj1: {c: 3, d: 100}, innerObj2: {e: 5}}
console.log(Object.is(oldObj, newObj));//falseconsole.log(Object.is(oldObj.innerObj1, newObj.innerObj1)); //falseconsole.log(Object.is(oldObj.innerObj2, newObj.innerObj2)); //true步驟分別為:
- 建立新物件
newObj並複製oldObj的所有屬性 - 因為想更新
oldObj內的innerObj1物件內的屬性d,因此也需建立一個新的innerObj1物件,並複製oldObj的innerObj1屬性,最後加上想覆蓋的屬性d - 沒有要更新
innerObj2的內容,因此不須另外產生新的物件參考,只需沿用既有的物件即可
此操作滿足了 immutable update 的要求:
- 既有資料所有層級的所有屬性值或參考都不能有任何改變
- 想更新既有資料中任何一層物件或陣列內容時,就要為了新資料產生對應的新參考
以解構賦值配合 rest 語法來剔除物件的特定屬性#
用解構賦值搭配 rest 語法,能以 immutable 方式剔除既有物件某一屬性:
const oldObj = { a: 1, b: 2, c: 3};const {b, ...newObj} = oldObj; //取出 b屬性,而 newObj 是除了 b 屬性以外的其他屬性所組成的物件
console.log(oldObj); //{a: 1, b: 2, c: 3}console.log(newObj); //{a: 1, c: 3}將想剔除的屬性 b 用解構賦值的方式單獨提出,將剩下的屬性用 rest 語法 … 集中到新物件 newObj 中,也就是除了想剔除的屬性外,複製既有物件的其餘屬性到新物件中。
陣列資料的 immutable update 方法#
以 spread 語法來插入陣列項目#
- 在陣列的開頭插入新項目
const oldArr = ['A', 'B', 'C'];const newArr = ['New Item', ...oldArr]; //先在新陣列放上要新增的項目,再用 spread 複製既有陣列的項目
console.log(oldArr); //['A', 'B', 'C']console.log(newArr); //['New Item', 'A', 'B', 'C']- 在陣列結尾插入新項目
const oldArr = ['A', 'B', 'C'];const newArr = [...oldArr, 'New Item']; //先在新陣列放上 spread 複製的既有陣列項目,再接著放要新增的項目
console.log(oldArr); //['A', 'B', 'C']console.log(newArr); //['A', 'B', 'C', 'New Item']- 在陣列中間插入新項目
以陣列的slice方法來複製兩段舊有陣列,因 slice 方法會回傳只包含部分項目的新陣列,且不 mutate 原陣列,可在 immutable update 時使用。
//想在 index 為 1 和 index 為 2 的項目中間插入新項目,代表目標插入位置是 index 2,//會將原先在 index 為 2 的項目 'C' 往後擠到 index 3
const oldArr = ['A', 'B', 'C', 'D'];
const insertTargetIndex = 2;const newArr = [ ...oldArr.slice(0, insertTargetIndex), //slice 不會 mutate 原有陣列本身,而會回傳新陣列,這裡會回傳 ['A', 'B'] 'New Item', ...oldArr.slice(insertTargetIndex), //這裡會回傳 ['C', 'D']];
console.log(oldArr); //['A', 'B', 'C', 'D']console.log(newArr); //['A', 'B', 'New Item', 'C', 'D']操作步驟:
oldArr.slice(0, 2)取得既有陣列 index2(不包含 index2)之前的所有項目,也就是['A', 'B'],將這段 spread 到新陣列開頭- 擺上要新增的項目
'New Item' - 再呼叫
oldArr.slice(2)取得既有陣列從 index2(包含 index2)以後的所有項目,也就是['C', 'D'],將這段 spread 到新陣列尾端
剔除陣列項目#
以陣列的 filter方法來剔除陣列中的特定項目,filter 通常用來過濾要保留的項目,轉換思路後,可想成是將不符合剔除條件的項目保留下來。
const oldArr = ['A', 'B', 'C', 'D'];
const removeTargetIndex = 2;const newArr = oldArr.filter((item, index) => index !== removeTargetIndex);
console.log(oldArr);//['A', 'B', 'C', 'D']console.log(newArr);//['A', 'B', 'D']更新或取代陣列項目#
以陣列的 map 方法來更新或取代陣列項目:
const oldArr = ['A', 'B', 'C', 'D'];const newArr = oldArr.map((item, index) => (index == 2) ? 'NEW Item' : item);//如果項目 index 符合目標條件就回傳新項目來取代,否則回傳原有項目
console.log(oldArr);//['A', 'B', 'C', 'D']console.log(newArr);//['A', 'B', 'NEW Item', 'D']也可用 map 作項目的進一步計算:
const oldArr = [550, 680, 250, 300];const newArr = oldArr.map(number => number * 0.9);
console.log(oldArr);//[550, 680, 250, 300]console.log(newArr);//[495, 612, 225, 270]排列陣列項目#
🚨 陣列內建的 sort 方法會 mutate 既有陣列,不能直接用 oldArr.sort() 來排序:
const oldArr = [5, 34, 22, 2];//⛔️ mutate 到舊陣列const newArr = oldArr.sort((a, b) => a - b);//直接以既有陣列進行由小到大排序,並回傳一個新陣列
console.log(oldArr);//[2, 5, 22, 34],既有陣列沒有保持 immutable,被 sort 方法 mutate 了console.log(newArr);//[2, 5, 22, 34]✅ 要以 immutable 的方式排序,先複製一份新陣列,針對新陣列 sort(),才能保持原陣列的 immutable:
const oldArr = [5, 34, 22, 2];const newArr = [...oldArr]; //用 spread 複製既有陣列的所有項目newArr.sort((a, b) => a - b); // sort 新陣列項目
console.log(oldArr);//[5, 34, 22, 2],既有陣列維持原樣,沒有被 mutateconsole.log(newArr);//[2, 5, 22, 34],新陣列被 sort() mutate 過而反轉陣列順序的 reverse方法也會 mutate 原陣列,一樣要先複製一份新陣列再 reverse 新陣列:
const oldArr = [5, 34, 22, 2];const newArr = [...oldArr]; //用 spread 複製既有陣列的所有項目newArr.reverse(); // reverse 新陣列項目
console.log(oldArr);//[5, 34, 22, 2],既有陣列維持原樣,沒有被 mutateconsole.log(newArr);//[2, 22, 34, 5],新陣列被 reverse() mutate 過巢狀式參考型別的複製誤解#
immutable update 巢狀物件或陣列資料時,常會出現錯誤的操作。範例程式碼如下:
import { useState } from "react";
export default function App() { const [cartItems, setCartItems] = useState([ { productId: 'foo', quantity: 1 }, { productId: 'bar', quantity: 8 }, { productId: 'fizz', quantity: 3 }, ])
const handleCarItemQuantityChange = (targetIndex, quantity) => { //更新 cartItems state 陣列中位於 targetIndex 這個 index 的物件的 quantity 屬性 //我們預期在呼叫 handleCarItemQuantityChange 後,更新位於 targetIndex 這個 index 的物件的 quantity 屬性 }此時多數人會以以下這種錯誤方式來更新 state,此方法仍會 mutate 原state 資料:
import { useState } from "react";
export default function App() { const [cartItems, setCartItems] = useState([ { productId: 'foo', quantity: 1 }, { productId: 'bar', quantity: 8 }, { productId: 'fizz', quantity: 3 }, ])
const handleCarItemQuantityChange = (targetIndex, quantity) => { //⛔️ 注意:以下是錯誤的、錯誤的、錯誤的 immutable update 方法!請不要讓它在你腦海停留 const newCartItems = [...cartItems]; newCartItems[targetIndex].quantity = quantity;
setCartItems(newCartItems) }巢狀式參考型別的 immutable update#
這個操作哪裡有錯?雖然用 spread 複製出新陣列,但陣列中每個項目都是物件,而物件以參考形式存在,因此新陣列中每個物件項目都和舊陣列的物件項目是同一個參考,mutate 新陣列中的物件項目,也會同時 mutate 既有陣列中的物件。
我們可以用 Object.is() 來查看 cartItems[targetIndex] 和 newCartItems[targetIndex] 是否指向同一個參考:
const handleCarItemQuantityChange = (targetIndex, quantity) => { //⛔️ 注意:以下是錯誤的、錯誤的、錯誤的 immutable update 方法!請不要讓它在你腦海停留 const newCartItems = [...cartItems];
console.log(Object.is(cartItems, newCartItems)); //false console.log(Object.is(cartItems[targetIndex], newCartItems[targetIndex])); //true
newCartItems[targetIndex].quantity = quantity; //newCartItems[targetIndex] 和 cartItems[targetIndex] 指向同一個物件參考,mutate newCartItems[targetIndex] 的 quantity 屬性就等同去 mutate cartItems[targetIndex] 的 quantity 屬性
setCartItems(newCartItems)}因為 newCartItems 是新陣列,在 setState 後以 Object.is() 檢查時,還是能被判定為不同參考而繼續 re-render,但實際上舊的 cartItems[targetIndex] 物件還是有被更改到,這也讓這個錯誤操作更難被察覺與除錯。
因此,在操作物件或陣列資料時,建議使用本來就是 immutable 的操作方法,如:map、filter、spread、rest。
以下是正確操作巢狀參考型別資料的方式,複製最外層,如果要更新內層的物件或陣列時,也都要複製並產生新的參考,而沒有要更新的則沿用即可:
const handleCarItemQuantityChange = (targetIndex, quantity) => {const newCartItems = cartItems.map((cartItem, index) => (//✅ 在 targetIndex 的位置產生新物件,複製既有物件的所有屬性,再覆蓋上 quantity 屬性的新值 (index === targetIndex) ? {...cartItem, quantity} : cartItem));
console.log(Object.is(cartItems, newCartItems)); //falseconsole.log(Object.is(cartItems[targetIndex], newCartItems[targetIndex)); //false
setCartItems(newCartItems);};sort 和 reverse 可以在複製陣列後呼叫而不擔心 mutate 的問題,是因為 sort 和 reverse 只會 mutate 陣列中項目的排列順序,不會 mutate 項目的內容,所以複製新陣列後再 sort 或 reverse 是安全的👌。
Spread 複製的是值還是參考?#
用 spread 語法複製物件屬性時:
- 如果是複製原始型別的屬性,會複製出獨立的新值
- 如果是複製陣列或物件的屬性時,會複製記憶體中的位址(參考),而非複製完整內容
以以下範例來說:
const oldObj = {a: 10, b: 20, c: { foo: 8, bar: 9}};const newObj = {...oldObj, d: 400};
console.log(newObj); //{a: 10, b: 20, c: { foo: 8, bar: 9}, d: 400}console.log(Object.is(oldObj.c, newObj.c)); //true複製屬性 a 和 b 時,因為 a 和 b 屬性是原始型別,所以是複製值本身,而屬性 c 是物件,所以是複製參考,因此 oldObj.c 和 newObj.c 會指向同一個參考,mutate newObj.c 的同時也會修改到 oldObj.c。
若要改動 oldObj.c,應該先複製一份 oldObj.c,再覆蓋新的屬性值:
const newObj = { ...oldObj, c: {...oldObj.c, foo: 800, buzz: 600}, d: 100}JavaScript 中,陣列或物件資料中每一層參考都是獨立的,spread 語法只會做單層的 shallow clone。
補充:shallow clone 和 deep clone 都是指複製物件或陣列的方式,但深度不同
- shallow clone (淺複製):只複製物件第一層屬性,如果屬性內容是原始型別則複製實際的值;如果屬性內容是物件型別則複製參考,複製的參考仍指向原物件。JavaScript 的 spread 語法是常用的 shallow clone 方式。
- deep clone (深複製):複製物件的所有層級,若遇到巢狀物件或陣列結構,會遍歷並複製每一層的每一個值。
Immutable update 不需要且不應使用 deep clone#
immutable update 巢狀陣列或物件時,不需修改的部分只需沿用舊的參考,因為 immutable 重點不是整包資料完整複製或獨立,不需要做 deep clone,重點是「有內容更新需求時,就要建立全新的參考;而沒有更新需求的就可沿用參考」,只要讓既有資料能永遠對應特定歷史時刻的狀態即可。
補充:不推薦以 deep clone 來 immutable update React 的 state 資料,會導致以下缺點:
- 消耗多餘效能
- 造成不必要的複製,導致記憶體和效能的浪費
- 失去參考相等性,React 效能優化機制依賴於參考的相等性,deep clone 會導致沒更新的地方也產生新的參考,被 React 誤以為資料有更新,導致優化機制出錯
當物件或陣列結構複雜後,可能需要以多層的 spread 來複製,會降低程式碼可讀性和可維護性,因此可透過第三方套件來協助我們處理 immutable update(但初學還是建議先從基本概念和操作學起):
Reference:#
如有任何問題歡迎聯絡、不吝指教✍️