illumy
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
      • Invitee
    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Versions and GitHub Sync Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
Invitee
Publish Note

Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

Your note will be visible on your profile and discoverable by anyone.
Your note is now live.
This note is visible on your profile and discoverable online.
Everyone on the web can find and read all notes of this public team.
See published notes
Unpublish note
Please check the box to agree to the Community Guidelines.
View profile
Engagement control
Commenting
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
  • Everyone
Suggest edit
Permission
Disabled Forbidden Owners Signed-in users Everyone
Enable
Permission
  • Forbidden
  • Owners
  • Signed-in users
Emoji Reply
Enable
Import from Dropbox Google Drive Gist Clipboard
   owned this note    owned this note      
Published Linked with GitHub
Subscribed
  • Any changes
    Be notified of any changes
  • Mention me
    Be notified of mention me
  • Unsubscribe
Subscribe
《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> ) ``` ![image](https://hackmd.io/_uploads/rypP1YYUyx.png) <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) ![image](https://hackmd.io/_uploads/BkIHIznUkx.png) # 2-9 React 畫面更新流程機制(reconciliation) - render phase 與 commit phase component 畫面管理機制的兩個階段 - render phase - commit phase - 畫面更新流程機制 reconciliation ![image](https://hackmd.io/_uploads/rkNTyKtIyg.png) <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 儲存 ![截圖 2025-04-25 凌晨12.25.45](https://hackmd.io/_uploads/SkvGIJOJeg.png) - 多個state 在fiber node 儲存 ![截圖 2025-04-25 凌晨12.27.06](https://hackmd.io/_uploads/HkDPUyuylx.png) 在使用useState時並沒有告知 React 每個state 的自定義名稱或key 因此需依照相同順序,才能維持機制正常 - 如使用key來定義state,可能產生以下情況 ![截圖 2025-04-25 凌晨12.39.30](https://hackmd.io/_uploads/H1xUt1Oyee.png) - 依照順序則能產生樹狀結構避免key衝突問題 ![截圖 2025-04-25 凌晨12.40.38](https://hackmd.io/_uploads/BkQct1_keg.png)

Import from clipboard

Paste your markdown or webpage here...

Advanced permission required

Your current role can only read. Ask the system administrator to acquire write and comment permission.

This team is disabled

Sorry, this team is disabled. You can't edit this note.

This note is locked

Sorry, only owner can edit this note.

Reach the limit

Sorry, you've reached the max length this note can be.
Please reduce the content or divide it to more notes, thank you!

Import from Gist

Import from Snippet

or

Export to Snippet

Are you sure?

Do you really want to delete this note?
All users will lose their connection.

Create a note from template

Create a note from template

Oops...
This template has been removed or transferred.
Upgrade
All
  • All
  • Team
No template.

Create a template

Upgrade

Delete template

Do you really want to delete this template?
Turn this template into a regular note and keep its content, versions, and comments.

This page need refresh

You have an incompatible client version.
Refresh to update.
New version available!
See releases notes here
Refresh to enjoy new features.
Your user state has changed.
Refresh to load new user state.

Sign in

Forgot password

or

By clicking below, you agree to our terms of service.

Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
Wallet ( )
Connect another wallet

New to HackMD? Sign up

Help

  • English
  • 中文
  • Français
  • Deutsch
  • 日本語
  • Español
  • Català
  • Ελληνικά
  • Português
  • italiano
  • Türkçe
  • Русский
  • Nederlands
  • hrvatski jezik
  • język polski
  • Українська
  • हिन्दी
  • svenska
  • Esperanto
  • dansk

Documents

Help & Tutorial

How to use Book mode

Slide Example

API Docs

Edit in VSCode

Install browser extension

Contacts

Feedback

Discord

Send us email

Resources

Releases

Pricing

Blog

Policy

Terms

Privacy

Cheatsheet

Syntax Example Reference
# Header Header 基本排版
- Unordered List
  • Unordered List
1. Ordered List
  1. Ordered List
- [ ] Todo List
  • Todo List
> Blockquote
Blockquote
**Bold font** Bold font
*Italics font* Italics font
~~Strikethrough~~ Strikethrough
19^th^ 19th
H~2~O H2O
++Inserted text++ Inserted text
==Marked text== Marked text
[link text](https:// "title") Link
![image alt](https:// "title") Image
`Code` Code 在筆記中貼入程式碼
```javascript
var i = 0;
```
var i = 0;
:smile: :smile: Emoji list
{%youtube youtube_id %} Externals
$L^aT_eX$ LaTeX
:::info
This is a alert area.
:::

This is a alert area.

Versions and GitHub Sync
Get Full History Access

  • Edit version name
  • Delete

revision author avatar     named on  

More Less

Note content is identical to the latest version.
Compare
    Choose a version
    No search result
    Version not found
Sign in to link this note to GitHub
Learn more
This note is not linked with GitHub
 

Feedback

Submission failed, please try again

Thanks for your support.

On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

Please give us some advice and help us improve HackMD.

 

Thanks for your feedback

Remove version name

Do you want to remove this version name and description?

Transfer ownership

Transfer to
    Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

      Link with GitHub

      Please authorize HackMD on GitHub
      • Please sign in to GitHub and install the HackMD app on your GitHub repo.
      • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
      Learn more  Sign in to GitHub

      Push the note to GitHub Push to GitHub Pull a file from GitHub

        Authorize again
       

      Choose which file to push to

      Select repo
      Refresh Authorize more repos
      Select branch
      Select file
      Select branch
      Choose version(s) to push
      • Save a new version and push
      • Choose from existing versions
      Include title and tags
      Available push count

      Pull from GitHub

       
      File from GitHub
      File from HackMD

      GitHub Link Settings

      File linked

      Linked by
      File path
      Last synced branch
      Available push count

      Danger Zone

      Unlink
      You will no longer receive notification when GitHub file changes after unlink.

      Syncing

      Push failed

      Push successfully