Deep clone 又稱深複製,相對淺複製僅是將物件的第一層複製,深複製則是將物件的所有層級都複製一份,深複製當遇到巢狀物件或是陣列時,就會進行深層的遍歷,將每一次層的值都進行複製,如此一來複製出來的物件當被修改時就不會影響到原來的物件。
JSON.parse(JSON.stringify(...))
const memberInfo = {
id: 120,
name: "Andy",
isVip: false,
birthday: "1996/01/12",
hobbies: ["Photography", "Cooking", "Painting"],
};
//先將 memberInfo 物件轉換成 JSON 字串,再將 JSON 字串轉換成物件
function deepCopy(item) {
return JSON.parse(JSON.stringify(item));
}
const deepCopyByJSON = deepCopy(memberInfo);
console.log("deepCopyByJSON === memberInfo", deepCopyByJSON === memberInfo); //false
console.log(
"deepCopyByJSON.hobbies === memberInfo.hobbies",
deepCopyByJSON.hobbies === memberInfo.hobbies,
); //false
但是使用 JSON.parse(JSON.stringify(...))
進行深複製時,如果物件內的屬性有不可以序列化的值,就會導致深複製失敗。ex: undefined
、function
、symbol
、BigInt
、Date
、RegExp
、Error
、Map
、Set
const memberInfo = {
id: 120,
name: "Andy",
isVip: false,
birthday: "1996/01/12",
hobbies: ["Photography", "Cooking", "Painting"],
getMoreInfo: function getMoreInfo() {
return null;
},
createdTime: new Date("2024-06-10"),
};
function deepCopy(item) {
return JSON.parse(JSON.stringify(item));
}
const deepCopyByJSON = deepCopy(memberInfo);
deepCopyByJSON.createdTime
跟memberInfo.createdTime
的值就不同了,因為Date
物件無法被序列化。
deepCopyByJSON.getMoreInfo
的值為 undefined,因為 function 也無法被序列化。
structuredClone(value)
const memberInfo = {
id: 120,
name: "Andy",
isVip: false,
birthday: "1996/01/12",
hobbies: ["Photography", "Cooking", "Painting"],
};
//使用 structuredClone 深複製物件 memberInfo
const deepCopyByStructuredClone = structuredClone(memberInfo);
console.log(
"deepCopyByStructuredClone.hobbies === memberInfo.hobbies",
deepCopyByStructuredClone.hobbies === memberInfo.hobbies
);//false
`deepCopyByStructuredClone.hobbies` 跟 `memberInfo.hobbies` 的 reference 不同
但是同樣的 structuredClone
也無法處理 function
、Date
、RegExp
、Error
、Map
、Set
等不可序列化的值。
const memberInfo = {
id: 120,
name: "Andy",
isVip: false,
birthday: "1996/01/12",
hobbies: ["Photography", "Cooking", "Painting"],
getMoreInfo: function getMoreInfo() {
return null;
},
createdTime: new Date("2024-06-10"),
};
const deepCopyByStructuredClone = structuredClone(memberInfo);
當物件內有不可序列化的值時,直接使用structuredClone
就會直接報錯。
//map(用於存儲已複製物件的 WeakMap),在 WeakMap 中,鍵必須是物件。
function cloneDeep(obj, map = new WeakMap()) {
//如果 map 中已經有 obj 的複製,則直接返回該複製。這可以防止循環參照導致的無窮遞歸。
if (map.has(obj)) {
return map.get(obj);
}
//首先排除非物件類型的,檢查傳入的 obj 是否為 null 或是原始型別,這是因為這些類型的值在 JavaScript 中是按值傳遞的,所以不需要複製。
if (obj === null || typeof obj !== "object" || typeof value === "function") {
return obj;
}
//如果 obj 是 Date 的實例,則創建一個新的相同的 Date 實例並返回。
if (obj instanceof Date) return new Date(obj);
//如果 obj 是 RegExp 的實例,則創建一個新的相同的 RegExp 實例並返回。
if (obj instanceof RegExp) return new RegExp(obj);
//函式輸出 output 值,如果 obj 是陣列,則輸出為空陣列,如果 obj 是一個普通物件,則 output 的原型會被設置為 obj 的原型,以保留原型鏈。
const output = Array.isArray(obj)
? []
: Object.create(Object.getPrototypeOf(obj));
//將 obj 和 output 的對應關係存入 map
map.set(obj, output);
//使用 Reflect.ownKeys(obj) 獲取 obj 的所有自有屬性鍵(包括符號和不可枚舉的屬性)
//遍歷所有的鍵,對每個鍵對應的值進行深度複製,並將複製的結果存入 output。
for (const key of Reflect.ownKeys(obj)) {
const val = obj[key];
//對當前鍵對應的值進行深度複製,並將複製的結果存入 output。
output[key] = cloneDeep(val, map);
}
return output;
}
const memberInfo = {
id: 120,
name: "Andy",
isVip: false,
birthday: "1996/01/12",
hobbies: ["Photography", "Cooking", "Painting"],
getMoreInfo: function getMoreInfo() {
return null;
},
createdTime: new Date("2024-06-10"),
};
const cloneDeepMemberInfo = cloneDeep(memberInfo);
console.log("cloneDeepMemberInfo.getMoreInfo", cloneDeepMemberInfo.getMoreInfo);
console.log("memberInfo.getMoreInfo", memberInfo.getMoreInfo);
console.log("cloneDeepMemberInfo.createdTime", cloneDeepMemberInfo.createdTime);
console.log("memberInfo.createdTime", memberInfo.createdTime);
簡單來說,使用 deep clone 操作物件資料會使 React 效能優化機制失效。
這裡就舉一個例子來進行說明
React.memo()
是一個效能優化的方法,它將 component 包裹起來,每當有 props 傳入 component 時會使用 Object.is()
檢查這次的 props 與前次的 props 是否相同,當 component 的 props 沒有變動時,就不會重新 render,這樣可以避免不必要的 re-render,提升效能。
在這個例子中,App component 中有一個被 memo() 包裹住的 Child component。
import { memo, useState } from "react";
function Child({ fooObj }) {
console.log("render Child");
return <h1>child: {fooObj.b}</h1>;
}
const MemoizedChild = memo(Child);
export default function App() {
console.log("render App");
const [data, setData] = useState({
count: 0,
foo: { b: 100 },
});
const updateCountWithDeepClone = () => {
// 使用 deep clone 複製 data 物件並且賦值給 newData
const newData = structuredClone(data);
// 更新 newData 的 count 屬性
newData.count += 1;
// 更新 data 的 state
setData(newData);
};
return (
<div>
<h2>count: {data.count} </h2>
<button onClick={updateCountWithDeepClone}>
Update Count With Deep Clone
</button>
<MemoizedChild fooObj={data.foo} />
</div>
);
}
當 component 第一次 mounted 掛載至瀏覽器後:
點擊 Update Count With Deep Clone button
會呼叫 updateCountWithDeepClone function
在 updateCountWithDeepClone function
中會使用 structuredClone
進行 deep clone data 物件並且賦值給 newData
更新 newData 的 count 屬性
更新 data 的 state
React 會執行 Object.is()
檢查發現 state 更新了
進入 component reconciliation 階段,執行 App function 產生以 props 和 state 描述 component 畫面的 react element,這時就會印出 render App
。
將新版本所產生的 react element 與上一次 render 的舊版 react element 進行樹狀結構的比較,找出差異就是 state,然後更新 <h2>count: {data.count} </h2>
。
當執行到 <MemoizedChild fooObj={data.foo} />
時,雖然 data.foo
的值看似沒有改變,但是 React 發現 props 有變,當 App Component render 完成後,就會執行 memoized Child component function,這時會印出 render Child
。
這是因為使用了 deep Clone 更新了 state 資料,又 deep Clone 所將物件中每一層的每一個屬性都經過遍歷複製,即使沒發生更新的內層資料也會產生全新的參考,因此,此時當使用 object.is()
檢查傳入 <MemoizedChild fooObj={data.foo} />
的 props 時,就會發現 props 有變動,所以會重新 re-render,進而印出 render Child
。
從上述可以知道,對於 React 來說,使用 deep clone 進行 state 更新會使得某些 React 效能優化的手段失效,導致每一次 state 的更新都是全新的物件、全新的參考,即使該屬性的值沒有變更,而使得效能優化機制失去參考相等性。
import { memo, useState } from "react";
function Child({ fooObj }) {
console.log("render Child");
return <h1>child: {fooObj.b}</h1>;
}
const MemoizedChild = memo(Child);
export default function App() {
console.log("render App");
const [data, setData] = useState({
count: 0,
foo: { b: 100 },
});
const updateCountWithShallowClone = () => {
setData({
...data,
count: data.count + 1,
});
};
return (
<div>
<h2>count: {data.count} </h2>
<button onClick={updateCountWithShallowClone}>
Update Count With Shallow Clone
</button>
<MemoizedChild fooObj={data.foo} />
</div>
);
}
當 component 第一次 mounted 掛載至瀏覽器後:
點擊 Update Count With Shallow Clone button
會呼叫 updateCountWithShallowClone function
在 updateCountWithShallowClone function
中會展開運算符淺複製 data 物件,然後更新 count 屬性,最後 setState 更新 data 的 state。
更新 count 屬性
最後 setState 更新 data 的 state
React 會執行 Object.is()
檢查發現 state 更新了
進入 component reconciliation 階段,執行 App function 產生以 props 和 state 描述 component 畫面的 react element,這時就會印出 render App
。
將新版本所產生的 react element 與上一次 render 的舊版 react element 進行樹狀結構的比較,找出差異就是 state,然後更新 <h2>count: {data.count} </h2>
。
最後的執行結果就是傳入 <MemoizedChild fooObj={data.foo} />
的 props 並沒有變動,所以不會重新 re-render,也就不會印出 render Child
,只有 App component 會重新 render。
對於 React 來說資料比較機制就是為了減少重複產生 react element 的次數,React 不需要知道實際的資料細節,檢查原始型別時直接互比值、檢查物件時只需要比對參考是否相同,當物件資料的參考相同,React 就會當作資料沒變,不管物件內容如何改變,React 都不會重新產生新的 react element 來 render,因此更新 state 時不應該 mutate 原始資料,而是應該產生新的物件這樣就會產生一個新的 Reference,透過 shallow clone 複製原始物件的屬性到新的物件,只要根據更新的部分去修改相對應的值,這樣既不會 mutate 到原始資料,維持物件 immutable,也可以提供 React 效能優化機制參考的相等性。