[TOC]
---
導讀人: 1t
筆記工:Cheryl
[[本週簡報連結](https://drive.google.com/file/d/1_tN0dfkuybOQJHfBZlWeAPVmZdrrui04/view?usp=sharing)]
---
# 2024/5/1 3-3~3-4
## 1. 導讀討論
### (1) 回顧React重要觀念
**2-6 單向資料流:『virtual DOM』與『一律重繪渲染策略』**
呼叫 setState 是使用 JS Object.is方法的觀念去判別更新

### (2) JavaScript Data Types

```===javascript
// 對應到下圖,此是 Before, o 和 obj 指向相同一個 obj
function add(obj) {
obj.number++
}
var o = {number: 10}
add(o)
console.log(o.number) // 11
```
```===javascript
// 對應到下圖,此為 After, 函式內對 obj 重新賦值
function add(obj) {
// 讓 obj 變成一個新的 object
obj = {
number: obj.number + 1
}
}
var o = {number: 10}
add(o)
console.log(o.number) // 10
```

在此 ,call by sharing 僅是複製 obj 的值宣告新的變數 obj,然後它被重新賦值,此時 o 和 obj 已經指向不同的記憶體位置。

想了解更多,可參考Huli 老師的文章 [深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?](https://blog.techbridge.cc/2018/06/23/javascript-call-by-value-or-reference/)
### (3) 使用 Object.is() 去判斷新舊資料差異
在 React 中,我們不該去 mutate 一個 object,該物件的 reference 並不會改變, Object.is() 判
別出物件沒有修改,就不會觸發 re-render。
**=> 應該要宣告新的物件 (擁有 new adress) 去取代舊的**

### (4) 保持 state immutable
* setState() 新舊資料檢查需求
* 過去 render 的舊state 仍有被讀取需求
* React 效能優化機制的參考檢查需求
* useCallback()、useMemo()、useEffect() 比較 dependency
直接更改物件,被 mutate 的物件導致的程式結果不如預期
舉例: 搭配到非同步的用法 setTimeout(),角色在移動時想加上一個提示是角色在 3秒前所待的位置。
[1t 的 codesandbox 範例](https://codesandbox.io/p/sandbox/qr-code-3-3-3-forked-qtt5dy?file=%2Fsrc%2FApp.jsx)
因為 mutate 了 position 的 state 物件,所以在提示 3 秒的時候 alert 出現的位置,會是當前的位置,因為歷史位置的 state 存取的 position 記錄已經被改掉了。
### (4) Immutable update 的手法
* 以 spread 語法複製內容
* {…object, A: a}
* 只複製第一層屬性(shallow copy),如果第一層是 object,複製的 reference 位置,如果複製的是 primitive typek 的話,就會複製值本身。
* 以 rest 和解構賦值的語法去複製並取出需要內容
* Const {A, …rest} = object

### (5) Nested reference 可能產生錯誤的用法
已經 mutate 了 position 這個物件,是錯誤的 mutable update

雖然宣告了新的陣列 newCart 並複製 cart 裡的值
但因為 Cart 是陣列裡的物件,spread operator 只是淺拷貝一層,內層還是指向相同位址, newCart[index].quantity = quantity 還是動到原本 Cart 的值。
```===javascript
// Cart
[
{productId: 'foo', quantity: 1},
{productId: 'bar', quantity: 100},
{productId: 'fizz', quantity: 3},
]
```

Immutable 的關鍵: 不在於深度複製所有層級的資料,而是沿用沒有內容更新需求的參考,新建內容更新需求的參考。
### (6) 深淺拷貝的差異 Deep Clone vs. Shallow Clone

immutable update 不需要也不應該使用 deep clone
* 效能考量
* 不必要的複製
* 失去參考相等性
(想了解可使用 deep clone 時機,可[點此文章](https://www.builder.io/blog/structured-clone))
## 2. 觀念自我檢測
### (1)在 JavaScript 的資料型別中,原始型別與物件型別的差異是什麼?
Robert: Primitive 資料型別(number, string, boolean)是存取資料值的本身。Object type: 物件, 陣列, 函式是存參考(address)
### (2)解釋什麼是 mutate 以及 immutable
Mi: mutate 是被建立可以被修改其值; immutable 是被建立後就無法被修改的,需要建立新值去取代舊值。
### (3)為什麼我們必須在 React 中去保持 state 資料的 immutable?
Amelie: 主要在於處理狀態跟追蹤的時候,這樣才可以追蹤到正確的狀態的時候去渲染出正確的值
### (4)Shallow clone 與 deep clone 的區別是什麼?
Var: Shallow clone 是複製物件的第一層屬性,如果第一層是原始型別會複製其值,若是陣列或物件的話,則會複製其參考,而不是實際的物件或陣列本身。
Deep clone 的話會複製物件的所有層級而不只是第一層,遇到巢狀的物件或陣列時也會遍歷每一層的每一個值,複製出來的值就會是新的資料,與舊的資料不同。
### (5) JavaScript 中 spread 語法的複製是 shallow clone 還是 deep clone?
(補充)HLTVProxy: 淺拷貝
### (6)為什麼以 deep clone 來進行物件或陣列資料的 immutable update 不是一個好方法?
Murmurline: 因為每一次做deep clone 的話會拷貝每一層完整的值,會影響到效能,只需要用淺拷貝去複製到需要相關的值就好。
有些 Senior 的工程師會使用一些第三方套件 mutate 的方式去做更新,如 lodash 等方案。
## 3. 問題討論
### (1) 是否有人實務上使用過「lodash」或 「ramda」嗎?
基本上大家都比較少用,組長分享: lodash 和 ramda 是 utilities 的函示庫,它個人實務上有用過 lodash 的一些方法。
### (2) 實務上有沒有必要用深拷貝的時候?
大家目前都沒有用過 deep clone,都是以 shallow clone 為主,可能是目前接觸到的專案都沒有此需求。
(Zet 的講解最後一部分,會說明此部分)
組長經驗分享: 主管直接將整包 lodash 引入,檔案變得很肥厚,若可以,請注意把需要使用到的 lodash 部分引入就好,效能優化會變好。
## 4. Zet 的講解
觀察三種按鈕不同父層、子層 re-render 的次數和情況
[用此 codesandbox: 看 memo, mutate 和深淺拷貝對效能的影響](https://codesandbox.io/s/immutable-update-shallow-deep-clone-kt253j)
### 理解什麼是 memo?
memo 是效能優化的一個方法:預設的作法就是每次的props 進來,都會用 Object.is()方法去做改變。
若傳進來的 props 沒有改變的話,就不會 re-render。 。
在此範例中,memo 放進要執行的 child function ,並宣告新的 MemoizedChild 變數, 即使父層 App 在 re-render, 但是因為 fooObj 使用 Object.is 方法返回 true,故此 child component 並不會被 re-render。
```===javascript
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,
});
}
const updateFoo = () => {
...
}
const updateCountWithDeepClone = () => {
const newData = structuredClone(data);
newData.count += 1;
setData(newData);
};
return (
<div>
<div>count:{data.count}</div>
<button onClick={updateCountWithShallowClone}>updateCountWithShallowClone</button>
<button onClick={updateFoo}>updateFoo</button>
<button onClick={updateCountWithDeepClone}>updateCountWithDeepClone</button>
<MemoizedChild fooObj={data.foo} />
</div>
);
}
```
### mutate 原有物件所造成的錯誤影響
方法 1. 點擊 updateFoo 按鈕沒有被更新的原因:
因為 newdata.foo.b +=1 使用 mutate 去改動原有的foo 物件(foo 物件指向原有記憶體位址的內容被改動),所以 React memo 在這裡用 Object.is() 去判斷時,會認為 MemorizedChild 傳下去的props 參考的位址相同,所以並無變動,子層的 Component 不會被更新。
前面不正確的mutate,在這父層雖重新渲染,但子層無法被正確更新資料,連帶重新渲染。
**這時候的效能優化反而或誤判,React 以為你不需要更新。**
方法 2 為正確做法,應該要產生一個新的物件,複製原本 data 的屬性以及要更新的物件該層的屬性(data.foo),然後將要更新的屬性使用賦蓋新的屬性值的方式更新。
```===javascript
const updateFoo = () => {
// (方法 1) 此做法會 mutate 原有物件 (是錯誤的!!!)
// const newData = { ...data };
// newData.foo.b += 1;
// setData(newData);
// (方法 2) 正確做法如下,使用覆蓋屬性的方法
const newData = {
...data,
foo: {
...data.foo,
b: data.foo.b + 1
}
};
setData(newData);
}
```
補充: mutate 物件,有時候可能剛好你以為沒有發生問題點,但其實可能只是剛好沒被發現,效能優化剛好讓你發現問題。
### 深拷貝對效能優化的影響範例
**思考: 當點擊 updateCountWithDeepClone 時,為什麼只更新 count, child 沒更新且已經使用 MemoizedChild 包住,還是一直被 re-render?**
因為深拷貝是複製每一層的每一個值,新的位址是完全新的變數,效能優化就會失去參考值,每次都認為深拷貝得值是新的,就會每次都重新渲染,比沒做效能優化還要差。
推薦做法: 在做巢狀物件的時候,就只修改必要的值,沿用沒有變更的值,才可有參考依據。
```===javascript
function Child({ fooObj }) {
console.log('render Child')
return <h1>child: {fooObj.b}</h1>;
}
const MemoizedChild = memo(Child);
export default function App() {
...
const updateCountWithDeepClone = () => {
const newData = structuredClone(data);
newData.count += 1;
setData(newData);
};
...
return (
<div>
<div>count:{data.count}</div>
<button onClick={updateCountWithShallowClone}>updateCountWithShallowClone</button>
<button onClick={updateFoo}>updateFoo</button>
<button onClick={updateCountWithDeepClone}>updateCountWithDeepClone</button>
<MemoizedChild fooObj={data.foo} />
</div>
);
}
```
**延伸題:
若改傳 data.foo.b 值呢? 為什麼現在 child component 不會被 re-render? 效能優化又好了?**
```===javacript
<MemoizedChild fooObj={data.foo.b} />
```
目前看起來是好的,只是剛好傳進的 props: data.foo.b 傳進去的值是原始型別是值跟值的比較,所以這檢查被認為是相同的值不會被重新渲染。
但若是物件和陣列傳進去,它比較其參考因為深拷貝而完全不同,還是會一直被觸發 re-render。
總結 => 還是盡量不要使用深拷貝
#### 思考問題: 什麼樣的情況需要做效能優化?
可以思考子層的 re-render 的資料是否很大,Child component 每次渲染的代價是否很昂貴?
若 Child component 本身資料很少,並不太需要做效能優化,因為效能優化機制的比較也是在消耗效能。
除非 Child component 每次渲染都要做很大量的運算或使用者體驗已經開始變差了,再去做優化。