owned this note
owned this note
Published
Linked with GitHub
《React 思維進化》閱讀筆記
# 2-1 DOM 與 Virtual DOM
1. DOM 的操作會綁定渲染引擎重繪畫面。React 透過比較新舊 Virtual DOM 差異、執行最小範圍的 DOM 操作,達到效能優化。
2. [Virtual DOM 效益釐清](https://www.zhihu.com/question/31809713):框架只是**足夠高效**,並更好維護
1. 框架的目的是提供抽象層,讓開發者不需直接操作 DOM,以[聲明式 declarative 的方式建立 UI(非宣告式 imperative)](https://react.dev/learn/reacting-to-input-with-state#how-declarative-ui-compares-to-imperative),從而讓程式碼更容易維護。
2. 因為多了一層框架 API,如 Virtual DOM Diff 的 JS 計算,不會比手動操作 DOM 高效(但大部分手動操作的程式碼也不是以最高效的方式操作 DOM,以渲染新的 list 為例)。
3. 框架提供足夠高效且可以處理所有可能更新場景的解法:
- React - Virtual DOM
- Vue - 依賴搜集
- Angular - 髒檢查 (dirty check)
- 性能比較需區分不同場景:
- 初始渲染:Virtual DOM > 髒檢查 >= 依賴收集
- 小量數據更新:依賴收集 >> Virtual DOM + 優化 > 髒檢查 > Virtual DOM(無優化)
- 大量數據更新:髒檢查 + 優化 >= 依賴收集 + 優化 > Virtual DOM
# 2-2 React Element:描述並組成畫面的最小單位
1. Virtual DOM 上的一個節點就是一個 React Element
```jsx
// 官網建議用 JSX 取代 createElement
React.createElement(
'h1', // 元素類型
{ className: 'greeting', key: 'unique' }, // 屬性
'Hello ', // 第三個參數後都是子元素
createElement('i', null, 'child'),
);
// React Element 是 JS 物件
{
type: 'h1',
props: { className: 'greeting', children: ['Hello', { type: 'i', ...} ] },
key: 'unique',
ref: null,
$$typeof: Symbol('react.element'),
}
```
2. **Treat React elements and their props as [immutable](https://en.wikipedia.org/wiki/Immutable_object)** and never change their contents after creation.
- React will [freeze](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) the returned element and its `props` property **shallowly** to enforce this.
3. [和 DOM element 的對應與差異](https://react.dev/reference/react-dom/components)
- inline style 改成 obj / `for` 改成 `htmlFor` 等。
- 補充 - Custom HTML elements (tag with a dash):
- Web Component 簡介:在任何現代瀏覽器中直接使用,具有跨框架互操作性。
| 特性 | Web Components | 框架元件 |
| ---------------- | ------------------------ | ----------------------- |
| **跨框架互操作性** | 高,適合跨框架使用 | 低,耦合於特定框架 |
| **樣式隔離** | 原生 Shadow DOM | 通過工具模擬實現 |
| **數據綁定** | 手動實現 | 框架內建支持 |
| **學習曲線** | 較高,需要熟悉原生 API | 較低,框架提供高級抽象 |
| **性能** | 高,但需要手動優化 | 高效,依賴框架內建的優化機制 |
```jsx
const App = () => {
return <my-custom-element class="my-class" for="input-id"></my-custom-element>;
};
// button 被當作自定義元素處理
const App = () => {
return <button is="my-custom-button" custom-attr="123"></button>;
};
```
4. 面試問題:component function 回傳的值是什麼?
- 答案:React Element,它是描述 UI 的 JavaScript 物件。
```jsx
// 編譯階段
const MyComponent = () => <div>Hello</div>;
↓
const MyComponent = () => React.createElement('div', null, 'Hello');
↓
{
type: 'div',
props: { children: 'Hello' },
// 其他內部屬性
}
// 運行階段:將 React Elements 組織成 Virtual DOM,經過 Diff 更新 DOM
```
# 2-3 Render React Element
章節目標:了解 React 如何將 Virtual DOM 的結構元素: React element, 轉換成實際的DOM element 並繪製到瀏覽器畫面
- 瀏覽器環境繪製
- 使用React 提供的`react-dom`
- 流程概念:指定瀏覽器畫面中的特定區塊,讓React對其擁有管轄權來持續進行 Virtual DOM 轉換為 DOM 的單向同步化
- 具體步驟:
1. 準備容器,用來輸出 Virtual DOM 轉換為後的 DOM 結果
```jsx
// index.html
<body>
<div id="root"></div>
</body>
```
2. 選取目標容器,用此元素透過呼叫 `ReactDOM.createRoot()`方法, 建立畫面渲染管轄的入口點(root)
```jsx
// index.js
import ReactDOM from "react-dom/client";
// 選取容器元素
const rootElement = document.getElementById("root");
// 建立 React App 用來管理DOM element 輸出結果的入口點
const root = ReactDOM.createRoot(rootElement);
```
3. 準備 React element
```jsx
// index.js
import React from "react";
const buttonReactElement = React.createElement(
'button', // HTML elements
{ id: 'button' }, // attribute
'按鈕' // child element
)
```
4. 透過 root.render() 將 React element 繪製成實際 DOM element
```jsx
// index.js
root.render(buttonReactElement);
```
5. 結果
```jsx
<body>
<div id="root">
<button id="button">按鈕</button>
</div>
</body>
```
- 補充
1. 1. <aside>
💡
`createRoot()` API[官方文件](https://react.dev/reference/react-dom/client/createRoot)
```jsx
const root = createRoot(domNode, options?)
// options => onCaughtError, onUncaughtError, onRecoverableError, identifierPrefix
// 回傳的物件包含兩個方法render, unmount
const { render, unmount } = root
```
</aside>
Q:為何要使用其他DOM,而不用 document.body 當 root 的容器元素?
A:React不建議主要是第三方套件時常針對 document.body 的子元素修改或操作,會造成React管理元素不穩定的情況,因此建議為React另開專門容器 (如果用 body 當容器,React 會在console 中報錯提醒)
<aside>
💡
Q:如果 root 裡有非 React element 的 DOM element 會如何?
A:會在root.render()後,被轉換後輸出的實際DOM覆蓋掉
</aside>
<aside>
💡
Q:root 只能有一個嗎?
A:React 也支援多個 root,但假如是 APP 是 SPA(Single-Page-Application)會建議只使用一個 root (補充其他多個root的情境)
</aside>
<aside>
💡
React DOM版本差異:
```jsx
// < React 18
import ReactDOM from "react-dom";
ReactDOM.render(buttonReactElement, rootElement)
```
</aside>
<aside>
🔥
**React 只會更新真正需要被更新的DOM element**
Virtual DOM 之所以能最小化DOM操作,主要是透過 React 對新舊 React element 的比對,避免產生不必要的DOM操作,讓開發者專注於管理React element
</aside>
- 瀏覽器以外的環境繪製
React 的畫面繪製是基於 兩階段(`Reconciler`, `Renderer`) 流程進行:
- Reconciler
定義及管理畫面結構描述
瀏覽器環境:建立新 React Element ,在有更新需求時比對新舊 React Element ,接著交給 Renderer
- Renderer
將畫面結構描述轉為實際畫面
瀏覽器環境:react-dom 產生的 root 繪製為實際的DOM,如後續 Reconciler 比較新舊差異時進行同步化工作
<aside>
💡
個人對2-3-3的理解是,因繪製拆分為兩步驟,reconciler 可通用(只要環境能跑JS),當 renderer 這階段 替換為支援目標環境的 renderer,就可以管理瀏覽器DOM以外的介面繪製,ex: APP 的 React Native, React-pdf 等等。
React: “**Learn once, write anywhere”**
</aside>
# 2-4 JSX 語法概念釐清
1. 使用 `babel` 或 `esBuild`(如 Vite 預設)等轉譯器,透過各自的 JSX transformer(例如 `@babel/plugin-transform-react-jsx`),將 JSX 轉為 JS。
- 補充:
- compiler:高階語言轉低階機器語言。
- transpiler:高階語言轉另一高階語言。
2. [React 17+ 提供 jsx-runtime](https://legacy.reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html),為 `createElement()` 的優化版。
- 新的 Babel JSX transformer 會在轉譯時自動引入。
- [@vitejs/plugin-react npm](https://www.npmjs.com/package/@vitejs/plugin-react):使用 automatic JSX runtime。
參考:[GitHub - esbuild PR #2349](https://github.com/evanw/esbuild/pull/2349)
# 2-5 JSX 語法規則與畫面渲染
條件判斷渲染:注意 0 雖然是 falsy,但數字會被轉成字串印在畫面上
```jsx
const MyFunc = ({num}) => (
// ...
{ 0 && <div>不會出現</div> }
// ...
)
```
章節目標:理解JSX語法的設計脈絡
- JSX 與 HTML 標籤閉合的差異
- Q:HTML 不一定需加 “閉標籤” 的原因
1. 標籤本身為「空元素」,只需要寫“開標籤”即可 ( ex: <img>, <input> )
2. 瀏覽器對 HTML 解析的容錯性,對於需要閉合的標籤,瀏覽器會自動推斷對應的閉標籤 ( ex: <div></div> )
<aside>
💡
自我閉合: `<div></div>` ⇒ `<div/>` 當標籤內沒有其他子元素,可以用此自我閉合方式表達
</aside>
- Q:JSX 一定需要進行閉合的原因
不管是自我閉合、還是加 閉標籤,JSX 一定要做閉合原因在於:
在 JSX transformer 進行轉譯成 `createElement()` 寫法時,
第三個參數 (指定子元素) 欄位,在缺乏閉合情況下,會無法正確解析 React element 層級關係
- 在 JSX 內表達 字串字面值 及 表達式 及 JSX 的差異
- 字串字面值:以雙引號包住 (ex: id="foo")
```jsx
const element = <div id="foo">字串字面值</div>
```
- 表達式:以大括號包住 (ex: onClick={handleClick})
```jsx
const handleClick = () => {
console.log('click')
}
const element = <div id="foo" onClick={handleClick}>表達式</div>
```
- JSX:是一種表達式,但可以不需要用大括號包住 (ex: <input />)
```jsx
const element = (
<div id="foo" onClick={handleClick}>
<input />
</div> // <input /> 是 React.createElement('input') 的表達式,但為更簡單表達,可省略大括號
)
```
- React Element 不同型別的子元素的處理方式
- React element:直接轉換為對應的DOM element
- 字串值:印出
- Boolean / `null` / `undefined` :忽略不印,作為判斷渲染使用
- Array:攤開子元素(如果子元素是可印型別)後印出
- Object / function :無法印出,報錯
- 畫面渲染邏輯
- 列表動態渲染
```jsx
const items = ['1', '2']
const element = (
<div>
{items.map((item)=> <li>{item}</li>)}
</div>
)
```
經過 transpiler 後 ⇒
```jsx
const items = ['1', '2']
const element = React.createElement(
'div',
null,
items.map((item) => React.createElement('li', null, item))
)
```
<aside>
💡
Key
列表渲染 需要為React element 填上不重複key,作為Virtual DOM 效能優化處理
</aside>
- 條件判斷渲染
1. if / else
2. &&
3. 三元運算
<aside>
💡
Q:JSX 語法第一層為何只能有一個節點?
A:因為 一個JSX 表達的是呼叫 一個 `createElement()` 的結果,也就是 一個React element 。故無法同時表達兩個React element
以下面錯誤程式為例:
```jsx
const elements = ( // error
<input />
<button />
)
```
多個根節點在樹狀資料結構來看是不合法的
為此,`Fragment` 即是 React 提供用來避免多餘的容器元素、不會渲染到實際DOM的解決方案。
</aside>
# 2-6 單向資料流
1. 單向資料流是什麼?畫面是原始資料透過模板與渲染邏輯產生的延伸結果。
2. 目的:資料與畫面可預測性良好且易於理解
3. 如何實現:「資料與畫面分離,資料驅動畫面更新」、「維持單一資料來源」
4. 方法一:資料與模板的綁定關係,因應資料更新的範圍,更新對應的 DOM,[範例](https://codesandbox.io/p/sandbox/96fzf5)。(Vue)
- 優:精準操作 DOM 減少效能浪費
- 缺:人為做複雜的綁定易有缺失
5. 方法二:資料更新後一律重繪,[範例](https://codesandbox.io/p/sandbox/ke7msp)。(React)
- 優:簡單直覺
- 缺:浪費效能
6. React 透過 virtual DOM 比較新舊結構差異,減少效能浪費
# 2-7 component 初探
1. 什麼是 component?根據需求將關心的特徵和行為歸納,抽象化成一套適用的流程或邏輯(具通用性和重用性)
3. 定義 component / 創建藍圖:透過函示表達「一段產生特定結構的 React Element 的流程」
4. 呼叫 component / 建立實例:實例之間彼此獨立、互不影響
5. props
- React 沒有限制 props 的資料型別
- [props 是唯獨且不可修改的](https://hackmd.io/sGFgW-5nSrmz9tgCjba97Q?both=&stext=1472%3A197%3A1%3A1735734479%3AdpdChb),有時 React 無法阻擋或警告,需要開發者自行注意
- 從函式參數解構出來的變數是以 const 定義的
```jsx
const MyFunc = ({num}) => {
num = num + 1 //會報錯,runtime error
// ...
}
```
- children: 不可以是物件或函式
### 2-7-6 ~ 2-7-8 父子 component 及 render
- component 內除了可包含實際DOM 的 React element 還可包含 其他component作為子元件
```jsx
// 包含實際 DOM
const Component1 = <div/>
// 包含 子元件
const Component2 = <Component3/>
const Component3 = <div>元件3</div>
```
- 為何 component 命名首字母需大寫
1. JSX transformer 的辨識需求,轉譯結果也會不同
2. 社群的命名慣例,方便其他開發者辨識是否是自定義元件
```jsx
// 舉例同上,該如何辨識標籤是表達:字面值 or 表達式?以大寫開頭區分
const Component1 = <div>元件1</div> // => React.CreateElement('div')
const Component2 = <Component3/> // => React.CreateElement(Component3)
```
- component render
以 component 呼叫,到執行 component function ,最終回傳描述轉譯後的 React element,稱之為“ 一次render ”
父層的 一次render 會執行到不再遇到子元件為止
- 流程範例
```jsx
const Component1 = () => (
<>
<Component2/>
<Component2/>
</>
)
const Component2 = () => (
<>
<Component3/>
<Component3/>
</>
)
const Component3 = () => (
<h3>h3</h3>
)
```

<aside>
💡
component re-render
內部狀態更新時發生,面對結構新舊差異,React 是一律重繪、而非更改原本 React element
</aside>
# 2-8 state 初探
1. 什麼是 state?
- 用於記憶狀態的可更新的資料,更新時觸發重繪
- 必須依附在 component 內,生命週期同 component
- component 的不同實例 state 互不影響
2. 如何使用 useState?
`const [state, setState] = useState(init)`
- 透過呼叫 setter function,觸發 re-render,即重新呼叫 component function 產生新的 react element(3-2 batch update, updater function)
- 依據固定的呼叫順序區別同個 component 內的 useState 呼叫,因此僅 top level 使用(不可放在迴圈、判斷式中)
- 回傳陣列而非物件,方便解構賦值
- React 官方文件:[State: A Component's Memory](https://react.dev/learn/state-a-components-memory)
3. `state` vs. `props` ?
- state:儲存 component 狀態
- props:傳遞資料
4. 補充:[How does React know which state to return? ](https://medium.com/@ryardley/react-hooks-not-magic-just-arrays-cd4f1857236e)

# 2-9 React 畫面更新流程機制(reconciliation)
- render phase 與 commit phase
component 畫面管理機制的兩個階段
- render phase
- commit phase
- 畫面更新流程機制 reconciliation

<aside>
💡
[Object.is()](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is)
靜態確認兩個值是否是相同值
</aside>
<aside>
💡
component re-reder
父層如果有 re-reder ,在沒有額外處理的情況下( 子元件用React.memo() 包.. 等 )
子元件也會跟著進行 re-reder
但 元件有 re-reder 不等於 實際 DOM 有被更新
</aside>
# 3-1 子元件觸發父元件資料更新
1. react 沒有特別設計向上溝通的機制,原本的規則就足以達成
2. 將 setter function 傳給子元件,或甚至傳給 react 以外的環境呼叫(如:redux、web api),都可以成功更新該 setter function 對應的 state,而 state 所屬的 component 也會 re-render
3. 子元件會因為父元件 re-render 取得更新後的 state
# 3-2
1. batch update:一個事件中呼叫多次不同或相同的 setter function 時,會自動依序合併 state 的更新結果,最後只 re-render 一次,節省效能。[範例](https://codesandbox.io/p/sandbox/determined-golick-vgkvk2)
- 什麼是 Batch Update
一次事件中呼叫多個setState,會將多個re-render合併為一次的節省效能機制
```jsx
const [state, setState] = useState(0)
const [state2, setState2] = useState(0)
const handler = () => {
setState(state+1);
setState2(state+2)
}
// 兩項都執行完才進行re-render
```
💡
如果不想自動Batch Update,可使用 `flushSync` 強制執行re-render 但要留意使用情境,避免不符合React渲染機制情形
```jsx
const [state, setState] = useState(0)
const [state2, setState2] = useState(0)
const handler = () => {
flushSync(
setState(state+1);
)
console.log(state)
// 要留意state此時還是0
}
```
- React 18 的改進
- React 18 前:只在事件處理函數中支援Batch update
- React 18 後:所有情況都支援(含 promise、setTimeout)
- 什麼是side effect
在函式執行過程中,對外部造成互動或修改 ex: 修改函式外的變數、操作DOM
<aside>
💡
Pure Function ⇒ 執行後沒有任何副作用的函式,
</aside>
<aside>
💡
Closure:js 基於同一 event handler 下會狀態將是固定不變
```jsx
const [state, setState] = useState(0)
const handler = () => {
setState(state+1);
setState(state+1);
setState(state+1);
}
// 最終state只會是: 1
```
</aside>
- Updater Function
- 使用時機:需要基於更新後的 state 計算新值時
- 正確用法:setState(prev =>prev + 1)
```jsx
const [state, setState] = useState(0)
const handler = () => {
setState(prev =>prev + 1); // state:1
setState(prev =>prev + 1); // state:2
setState(prev =>prev + 1); // state:3
setState(100); // state:100
}
// 最終得到 state:100
```
<aside>
💡
所以假設元件只需要變更值,而不需把值渲染到畫面時,props就只需要傳入setState即可
</aside>
# 3-3 immutable State:維持資料可靠性
1. 複習基本觀念
- 原始型別:immutable,變數只能透過重新賦值更新
``` js
const str = 'React';
str[0] = 'V'
console.log(str) // 'React'
```
- 物件型別:mutable
2. React 開發須遵守資料 immutable,應產生全新物件傳入 setter function。[範例](https://codesandbox.io/p/sandbox/qr-code-3-3-3-forked-y7hh9z?file=%2Fsrc%2FApp.jsx%3A3%2C32)
- 為正確觸發 re-render
- 為正確讀取舊資料。情境:文章編輯器 redo、undo 功能
- 讓效能優化機制正確作用,如:useCallback、useMemo (參考沒變就不會重新觸發處理)
# 3-4 immutable update
更新 state 應該要以 immutable 方式更新
以產生新的陣列或物件,而不是用 mutate 方式更改原有物件
<aside>
💡 state代表的是對應某個特定歷史時刻的值,因此state不應該以 **修改** 處理
</aside>
- 物件的 immutable update 方法
- 用 spread 語法複製後,增加新的屬性
```jsx
const oldObj = {a: '1', b: '2'}
const newObj = {...oldObj, a: '3', c:'4'}
// newObj = { a: '3', b: '2', c:'4'}
```
- 巢狀物件的 spread 寫法
```jsx
const oldObj = {a: '1', inner: { c: '2', d: '4' }}
const newObj = {...oldObj, inner: {...oldObj.inner, d: '5'} }
// newObj = { a: '1', inner: { c: '2', d: '5' }}
```
- 踢除物件中特定屬性( 賦值解構 + rest語法)
```jsx
const oldObj = {a: '1', b: '2'}
const {a, ...newObj} = oldObj
// newObj = { b: '2' }
```
<aside>
💡
[spread](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) 與 [rest](https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Functions/rest_parameters) 語法的寫法看起來完全相同,但實際作法則是相反
spread ⇒ 將物件或陣列的元素攤平
rest ⇒ 收集多個元素並「壓縮」為單一元素
</aside>
- 陣列的 immutable update 方法
- spread 語法
```jsx
// 在開頭加值
const oldArr = [1, 2]
const newArr = [3, oldArr]
// 在中間加值 => slice
const oldArr = [1, 2, 4, 5]
const newArr = [...oldArr.slice(0, 2), 3, ...oldArr.slice(2)] // [1, 2, 3, 4, 5]
```
- filter, map, slice 都會返回新陣列
- 陣列的 mutate 方法
以下兩種方法皆會mutate 既有陣列,必須先複製後再使用方法處理
- sort
- reverse
- 需要留意的巢狀複製
```jsx
const [ state, setState ] = useState([
{product: 'foo', price: 100},
{product: 'bar', price: 100},
{product: 'fizz', price: 100},
])
// 單純spread陣列來修改會mutate到既有state,因為物件是參考型別,複製出來的仍是舊有陣列同一批的物件參考
const newState = [...state ]
newState[0].price = 200 //錯誤
setState(newState)
// 正確方式
const newState = state.map((obj, index)=> index===1 ? {...obj, price: 200 } : obj )
setState(newState)
```
<aside>
💡 deep clone v.s. shallow clone
immutable update並不需要deep clone ,如果沒有修改則沿用舊參考
主要因效能考量、及會有失去參考相等性,造成 React 認為值有更新的情況
</aside>
# 4-1 Component 生命週期
1. Mount:Component 首次出現時
a. render phase:`JSX -> React Element -> Fiber Node`
b. commit phase:建立 DOM element 執行 appendChild()
c. 執行本次 render 對應的 effect
2. Update:component re-render/reconciliation
a. 以新版本 props, state 重新跑過 `JSX -> React Element -> Fiber Node`
b. 比較新舊 Fiber Tree,標記變更提交給 commit phase
c. commit phase:只更新差異處
d. 執行 effect 的 cleanup function
e. 執行本次 render 對應的 effect
3. Umount
a. 執行 effect 的 cleanup function
b. 移除 DOM element
c. 清除該 component 的 fiber node
- 補充 fiber,[參考](https://medium.com/starbugs/react-%E9%96%8B%E7%99%BC%E8%80%85%E4%B8%80%E5%AE%9A%E8%A6%81%E7%9F%A5%E9%81%93%E7%9A%84%E5%BA%95%E5%B1%A4%E6%9E%B6%E6%A7%8B-react-fiber-c3ccd3b047a1)
1. react 16+ 推出,是將 source code 重構後的新架構(本質上是 JavaScript 物件)
2. 優點:將頁面渲染的任務切分成 chunks、任務區分 priority、任務可以暫停再繼續、效能提升
- 如何達成:
- React15 及以前,Reconciler 階段採用遞迴的方式創建 virtual DOM。缺點是遞迴不能中斷且是同步的,如果元件樹的層級很深,會佔用 main thread 很多時時間,造成頁面卡頓等
- 在 fiber 架構下使用 linked list 資料結構遍歷 component tree
4. 什麼時候產生? `JSX -> React Element -> Fiber Node`
透過 `createFiberFromTypeAndProps` 方法
```tsx
const fiberNode = {
type: MyComponent, // Component 的類型(Function / Class / HTML tag)
key: null, // React 的 key 屬性
stateNode: null, // 對於 Class Component 來說,這裡是實例
child: null, // 指向第一個子 Fiber
sibling: null, // 指向下一個兄弟 Fiber
return: parentFiber, // 指向父 Fiber
alternate: null, // 指向舊的 Fiber(用於 Reconciliation)
effectTag: "Update", // 代表這個 Fiber 需要做的變更(新增、刪除、更新)
pendingProps: {}, // 這次 render 會使用的 props
memoizedProps: {}, // 上一次 render 使用的 props
memoizedState: null, // 上一次 render 的 state
}; ```
# 4-2 fuction component vs. class component
1. 在 class component 中雖然 props 是 mutable,但 this 不是,因此使用 this.props 會取得最新的資料,導致非同步事件中讀取到錯誤版本的 props。 [差異範例](https://codesandbox.io/p/sandbox/3jy2t5)
2. function component 則會取得該次 render 的 props 跟 state
# 4-3 每次 render 都有自己的props、state、event handler 函式
function component 每次render都有自己版本的 props、state、event handler
**資料來源**:這是與class component最大的差異
- class component:
資料從 this.props 來,但 this 本身是 mutable 物件,因此有時可能新舊值時間差、造成顯示與預期不符的狀況。
- function component:
React 實際並沒有監聽資料的變化,而是在每一次觸發 setState 時,使用Object.is()進行比較。
如有差異,則以當下最新的 props、state 再一次執行 component function
- 每次 render 也有 自己版本的 event handler function
核心機制主要是來自於 js 的 `closure`
⇒ 基於 closure 去記住當下使用到的props、state
因此每一次render 所產出的,都是只有命名相同 但 獨立、互不相關的 function
```jsx
function Component(){
const [ state, setState ] = useState(0)
const handleClickAlert = () => {
settimeout(()=>{
alert(`${state}`)
}, 3000)
}
const increment = () => { setState(state+1) }
// 先點擊alert按鈕,再馬上按+1按鈕 => 3秒後的alert仍會顯示0
return(
<button onClick={increment}>+1</button>
<button onClick={handleClickAlert}>alert</button>
)
}
```
---
<aside>
💡
immutable資料及函式closure的特性是維持React核心觀念:一律重繪的重要基礎。
每次render ,props 及 state 都有當下時期獨立且不變的快照版本,因此連帶函式也變得穩定且可預期。
</aside>
因此對開發者非常重要,需要有意識的去維持資料是immutable,維持整體的單向資料流
# 5-1 effect 初探
1. effect(副作用):和外部系統互動,如:存取函式外的變數、發起網路請求、修改 DOM element 等
2. 副作用帶來的負面影響:可預測性降低、測試困難/高耦合度(需要模擬或隔離外部資源)、增加維護和理解的成本、優化限制
3. React Component Function 中副作用的負面影響:
- re-render 時函式多次執行,造成副作用影響難以預測
- 副作用可能拖慢或阻塞本身產生 react element 的流程,如:修改 DOM element 還牽涉到瀏覽器渲染引擎
4. 解法:使用 useEffect 處理副作用
- 可以透過 cleanup 函式清除逆轉或副作用造成的影響
- 管理副作用執行的時間點(render 流程完後)
5. [撰寫步驟](https://react.dev/reference/react/useEffect)
a. 定義 setup function
b. 加上 cleanup function:避免 memory leak。
c. 加上 dependencies
- 跳過不必要的副作用處理
- 值為 setup 中有使用的 reactive values(props, state, variables and functions declared inside your component. can’t choose!)
- 建議使用 react eslint 檢查漏掉的 deps
- [比較 array, empty array, no dependency array](https://react.dev/reference/react/useEffect#examples-dependencies)
6. 每次 render 都有該次 render 的 setup/cleanup function,和 event handler 同樣都是 render 輸出結果的一部分,會因為閉包特性捕捉該次 render 的 props 和 state 快照。
7. 執行流程(re-render 一次為例)
a. 元件初次掛載到 DOM 上,跑 `setup1`
b. re-render 時,
- 先跑 `cleanup1`(code runs with the old props and state.)
- 再跑 `setup2`(code runs with the new props and state.)
c. 元件 unmounts 時,跑`cleanup2`
# 5-2 useEffect 不是 function component 的生命週期API
useEffect真正的用途:將原始資料同步到畫面以外的副作用處理
- 為何部分開發者會有useEffect是 生命週期API 的思維?
1. 早期class component 設計,副作用會在 `componentDidUpdate` 、 `componentDidMount` 等生命週期API處理,因此有經驗的開發者會認為useEffect執行時機類似,而將它理解為生命週期API。
2. 早期官方文件有提及:useEffect可以模擬class component 的生命週期API
可能是為了讓使用者更快的過渡class component 到 function component的說法,但實際上是一種誤導,後來這個內容也移除了。
- 宣告式設計 v.s. 指令式設計
- 宣告式設計:只關注於預期的“結果”,不在乎過程。
- 指令式設計:著重於達到目標的“過程”。
以維護及資料流方面,指令式設計因為是多個步驟疊加,較難一眼看出預期結果; 宣告式設計則較容易。
---
<aside>
💡
React的宣告式設計與 useEffect 的關聯?
useEffect並不在乎副作用的處理流程(mount \ update…)、或是渲染次數多寡,都應該在資料同步後到副作用產生的結果正確。
</aside>
----
- 為何生命週期API被取代了?
以往生命週期API的開發方式,養成了開發者必須依照個生命週期階段拆解動作、來達成資料同步的效果,但這種開發方式容易因為遺漏某一階段的處理而產生錯誤。
所以useEffect 才會在每次render 後都執行,以確保副作用是隨資料流變化而同步的
- `副作用應該是就算render後每次都執行,依然都要能保持正確性`
所以,想用依賴項來控制副作用執行時機是錯誤的思維
Dependencies應該用優化效能角度去思考,判斷何時跳過執行副作用是安全的,再加入對應的依賴項。
- 錯誤思維範例:我只想執行一次effect函式,所以依賴項放空陣列。
- 正確思維範例:effect函式真的沒有任何依賴,所以依賴項放空陣列。
---
<aside>
💡
如果副作用真的有特定商業邏輯,應該是在effect函式中用條件判斷處理
</aside>
---
---
💡補充文件
[https://overreacted.io/a-complete-guide-to-useeffect/](https://overreacted.io/a-complete-guide-to-useeffect/)
---
# 5-3 不要欺騙 hooks 的 deps
1. 欺騙 hooks 的問題:無法取得該次 reder 的 props 或 state
2. 使用 updater function,移除 deps(讓 effect 函式自給自足)
[1, 2 範例](https://codesandbox.io/p/sandbox/qr-code-5-3-1-forked-jd4l6j)
3. 如何處理函式型別的依賴
- 將函式定義在 effect 裡
<!-- <br/> -->
Q: 如果不想把函式放在effect?
1. 方法一:將函式內容抽到component外部 =>可避免重覆寫依賴項,達到效能優化
2. 方法二:用useCallback包,並填寫正確的依賴項=>將資料流的變動正確更新到函式上,如沒有變動函式則不會發生改變
:warning: 並不是所有component內的函式都要用useCallback包.以下情境需要:
1. 函式被使用在effect中
2. 作為props被傳遞到React.memo()包裹的component
> 建議安裝linter,判斷修正hook dependencies
> eslint-plugin-react-hooks、VS Code plugin-ESLint
:fire: <b>應始終對依賴項保持誠實,不應依想要執行的情境而選擇放入的依賴項</b>
# 5-4 effect 在 mount 時會執行 2 次
1. react 18 的 breaking change,為了檢查副作用的安全可靠性
2. react 未來的版本規劃: component 要設計足夠彈性,多次 mount, unmount 都不會壞,達到 resuable state。[參考](https://github.com/reactwg/react-18/discussions/19)
3. 補充:現有的 HMR (Hot Module Replacement)機制就符合這種特性:更新存檔時不需要重整就套用 component 的變動
- vite / react CRA / webpack 等內建 HMR 機制
- 實現原理
1. vite 使用 chokidar 等工具監控元件是否修改,在 dev server 和 browser 間建立 websocket 連線(可切到 network 的 ws 看)
2. vite dev server 發現元件變化後,重新編譯該元件,並透過 websocket 通知瀏覽器進行更新
3. vite 內建的 HMR js 接收到 websocket 訊息,會執行元件替換
- Fast Refresh 是 React 團隊針對 HMR 的強化版本,可以保留 component state( vite 2+ 開始支援,透過 @vitejs/plugin-react 這個官方插件來實現)
- 實作:把 `plugins: [react()]` 移除, state 不會保留
- HMR 歷史
- 2015 Webpack 1 引入 HMR 概念,開發者能修改 JS 模組並自動更新,不需整頁重新整理。
- 2016+ React Hot Loader 被廣泛使用來支援 React 的 HMR,保持元件 state。
- 2020 Vite 登場,利用原生 ES 模組(ESM)加上更快的模組熱更新,支援 out-of-the-box HMR,省去了複雜配置。
4. 為什麼要達到 resuable state?
- 支持 Fast Refresh:Fast Refresh 每次儲存檔案都會觸發 effect 重跑
- 支持未來的特性,如:Offscreen API
- 讓 component「離開畫面但保留狀態」的機制 (還處於 lab 階段),[使用場景](https://react.dev/blog/2023/03/22/react-labs-what-we-have-been-working-on-march-2023#offscreen-rendering)
- A router can prerender screens in the background so that when a user navigates to them, they’re instantly available.
- A tab switching component can preserve the state of hidden tabs, so the user can switch between them without losing their progress.
A virtualized list component can prerender additional rows above and below the visible window.
- When opening a modal or popup, the rest of the app can be put into “background” mode so that events and updates are disabled for everything except the modal.
- 不是靠 CSS display: none,而是 React 本身不渲染到畫面上,但保留整個 virtual DOM 與 state,在需要時快速還原並再次 mount
- **元件在生命週期裡 mount 和 unmount 不只一次,即使 deps 資料沒有更新,effect 仍可能執行**
# 5-5 副作用處理的常見情境
- 常見副作用的設計問題
1. 疊加而非覆蓋的操作,造成多次執行後結果不如預期
2. race condition,如:請求回傳速度不一
3. memory leak,如:監聽事件未被取消
處理方式
1. fetch api:透過宣告 flag 解決 race condition。建議使用第三方套件,如: react query、swr、react apollo
2. 避免外部套件重複初始化
``` tsx
useEffect(() => {
if(!ref.current) ref.current = thirdPartyLib.init()
}, []);
```
3. 需頻繁將資料同步到第三方套件時,可以加上 throttle 等效能調校處理
``` tsx
// 使用 throttle 控制每 500 毫秒只調用一次 fetchData
import { throttle } from 'lodash';
const throttledFetchData = throttle(fetchData, 500)
```
4. 監聽或訂閱事件,需加上 cleanup function,如: click event、setInterval
# 5-6 useCallback & useMemo
- 函式在function component遇到的問題
當在function component所建立的函式,在 `useEffect` 被呼叫時,依賴項即使正確的寫了此函式,也並不能達到效能優化
```jsx
function SearchResults(props) {
async function fetchData(query) {
const result = await axios(
`https://foo.com/api/search?query=${query}&rowsPerPage=${props.rows}`,
);
}
useEffect(
() => {
fetchData('react').then(result => { ... });
},
[fetchData]
// 雖然依賴有寫,但因為每次rerender都會重新產生新的fetchData,無法達到優化
);
// ...
}
```
- useCallback
- 運作原理
會在每一次render建立一個以當次state資料的函式,再依照依賴項去比對,如果依賴項沒有更新,則回傳快取的函式,有更新則使用新建立的函式。
```jsx
function SearchResults(props) {
const fetchData = useCallback(
async (query) => {
const result = await axios(
`https://foo.com/api/search?query=${query}&rowsPerPage=${props.rows}`,
);
return result;
},
[props.rows]
);
useEffect(
() => {
fetchData('react').then(result => { ... });
},
[fetchData]
);
// ...
}
```
---
🔥 useCallback本身的運作原理並不算節省效能
因爲不論新舊的依賴項是否相同,仍會先依照新版本及舊版本的狀態去產生對應的函式,
後續才會進行比對判斷要回傳哪項函式
---
- 使用情境
當在function component所建立的函式,需要在useEffect 中使用,或需要作為props被傳入子元件時,則建議使用useCallback包.
---
🔥React.memo
如傳入props沒變化則略過render流程,回傳快取的render結果。
此時如props有包含在父層元件裡建立函式,且沒有使用useCallback包住,也會導致效能優化失敗.
---
- useMemo
作爲效能優化的工具,會比對依賴項的差異,如相同則回傳快取,不同則使用新值.
假設在function component 裡建立一個陣列的 state,當其被列在useEffect依賴項時,因每次rerender時陣列都會重新被建立,而失去效能優化作用,因此這種情境就該使用useMemo包.
```jsx
import React from 'react';
function Child(props) {
return (
<>
<div>Hello, {props.name}</div>
{props.numbers.map(num => (
<p>{num}</p>
))}
</>
);
}
const MemoizedChild = React.memo(Child);
function Parent() {
const numbers = [1, 2, 3]; // 每次render都會建立新的陣列
useEffect(
() => console.log(numbers),
[numbers]
);
return (
<MemoizedChild
name="zet"
numbers={numbers}
/>
);
}
```
```jsx
// 應該改為如下:
const numbers = useMemo(()=>[1, 2, 3], []);
```
# 5-7 hooks 設計原理
1. fiber node:負責儲存目前React 應用程式的最新狀態資料,只會存在一份
React element:用於描述某個歷史時刻的畫,會隨著rerender 產生好幾份(Q:怎麼看前幾分的react elements?)
2. fiber node 中以 linked list 存放狀態資料,故會需要固定 每次 render 時 hook 的呼叫順序
3. 不讓 hook 執行:unmount 該 component
4. hook 的設計是為了管理狀態並方便共用邏輯,故選擇以函式為載體。不需要基於 key,避免覆蓋或重複呼叫的衝突
- 單一state 在fiber node 儲存

- 多個state 在fiber node 儲存

在使用useState時並沒有告知 React 每個state 的自定義名稱或key
因此需依照相同順序,才能維持機制正常
- 如使用key來定義state,可能產生以下情況

- 依照順序則能產生樹狀結構避免key衝突問題
