🔗 [第一組章節分工](https://hackmd.io/Jn-rU9Y8QYCMj5yCo0robw) [TOC] # 2024/3/17 2-6 ~ 2-7 簡報: - 2-6 單向資料流與一律重繪渲染策略: https://hackmd.io/@NoName21/S1XpSzspa#/ - 2-7 畫面組裝的藍圖:component 初探: https://hackmd.io/@NoName21/B1PMLS4Cp#/ 導讀人: Aya 筆記工: Lois ## 問題討論 Q: 每個檔案最多可以有幾個 default export ? A: 1 個 Q: UI 跟 Component 的區別 A: UI = 畫面 = 使用者介面,不是在指某個機制 Q: 策略二的一律清空畫面是模擬 React 的行為嗎? A: 不是,2-6 在講實現單向資料流的 DOM 渲染策略都不是在指任何一個框架,而是在純 JavaScript 情境中能夠實現單向資料流的常見策略 Q: React 官方文件提到的 one-way data flow 是指它的東西會從最上層傳到最下層,還是指資料與畫面分離,這兩件事是同一件事嗎 A: 這是在講同一件事,但這裡不是在講單向資料流本身的意義,而是單向資料流在 React 中要做出的效果,這句話其實就是單一資料流的結果 額外補充: - React 官方文件都只講結果,不講概念的本質,因為閱讀門檻高,就像翻遍官方文件找不到 React element 的詞,唯一能看到 React element 的詞只有在 createElement 可以找到,因為他放棄治療解釋什麼是 React element,所以現在都用 JSX element 這個說法在指 React element,反正沒有人會手動寫 React.createElement ,大家都寫 JSX,但真的懂 React 的人不可能不知道 React element - `<Component/>` 跟 `{Component()}` 兩者呼叫的差異 - 假設今天有一個 Foo component ```js const Foo = () => { return ( <div> <h1>Foo</h1> </div> ); }; ``` - 問題一:這個 console.log 出來的結果會是什麼? ```js const a = <Foo />; console.log(a); ``` A. ```js <div> <h1>Foo</h1> </div> ``` B. ```js <Foo/> ``` 答案是 ||B|| - 問題二:當程式執行到這段時,`<Foo />` 是否已被呼叫? ```js const a = <Foo /> ``` 答案是 ||沒有|| - 這兩種不同的呼叫方法的差異: ```js const a = <Foo /> const b = Foo() console.log(a) // {type: f Foo(), ...} console.log(b) // {type:"div", ...} ``` - 這兩種呼叫方法最大的差異是,你必須透過建立 React element 的方法才能讓他被當作 component 來呼叫,這是唯一的方式 - 如果是用 Foo() 來呼叫,React 只會當作它是一個普通的 funcion,它只是一個首字母為大寫的 function - React 並不是以首字母為大寫來判斷是不是 component - 首字母大寫對 React 來說沒有意義,對 transpiler 才有意義,因為最後 React 都會執行 React.createElement - 假設今天有一個小寫 a 作為 component ```js const a = () => { return <div/> }; ``` 如果是使用 React.createElement 來呼叫 a ,仍會被當作是一個 component,只是不能寫 JSX,因為如果寫 JSX ,它會以為是一個 DOM element 類型 - 所以能讓 React 正確的判斷是不是 component 是以呼叫的方式來決定,而不是由定義的方式來決定 - 若使用下方這段程式碼,並且在 React 開發者工具來觀察 ```js const Foo = () => { return ( <div> <h1>Foo</h1> </div> ); }; root.render( <div> <Foo /> {Foo()} </div> ); ``` 就會發現,使用 Foo() 來呼叫在結構上並不會有 Foo 這個節點 ![image](https://hackmd.io/_uploads/B1k7ej4Cp.png) 所以其實使用 Foo() 來呼叫就等同於直接將 React element 填入 ```js root.render( <div> <Foo /> {Foo()} </div> ); ``` 等同於: ```js root.render( <div> <Foo /> { <div> <h1>Foo</h1> </div> } </div> ); ``` - 為何要使用 react element 的方法去呼叫 component? - 這是一種 React 延遲呼叫的設計,背後還有一個概念是控制反轉 - 白話文是:我這裡想要一個 component 的實例,你幫我產 - `<Foo />` 只是在描述我希望這裡產生一個 Foo component 實例的這件事,但還沒被呼叫 - 當 component function return 了 react element 之後,reconciler 會開始進行分析如何將 react element 轉成 DOM element,當遇到不是 DOM element 的東西的時候才會去呼叫 - 由上而下由外往內的流程產出 react element 的 tree 之後才會交給 renderer 一次轉換成實際的 DOM element - 子 component 的呼叫不是在父 component 建立 react element 的過程,會等到描述完了內部開始處理才開始呼叫 - 所以使用 `{Foo()}` 來呼叫,在 runtime 時 JavaScript 引擎分辨不出這裡要建立一個 component 的實例,只是單純在描述一段 React element 的表達式 - 為什麼 component 能夠帶有狀態,是因為 React 對 hooks 加料,能夠去讀取 component 內部的資料,所以直接使用 call function 去呼叫 component,就不是在 React 機制底下運作,就會導致 hooks 壞掉 - 如果不是 component,則完全可以使用一個變數來描述一段 react element,甚至可以根據 props 傳入產生不同的 react element ```js const renderTextElement = (text) => <p>{text}</p> ``` - 延遲呼叫還有一個好處是當我去建立這個 React element ,我可以知道你想呼叫什麼 component ,甚至知道你想傳入什麼 props ,React 其實會根據某些情況判斷不一定要去 render 這個 component - 如果將子 component 定義在父 component 之外,並使用`{}` 來呼叫,即使父 component 被 re-render,該子 component 因為被判定為同一個 component 為不會被 re-render - React 有一個內建的優化機制,發現其中有一個節點是 component 類型的,並且在兩次 render 之間為同一個 component 類型,則會略過這個 re-render ```js import React, { useState } from "react"; function Foo() { console.log("render Foo"); return ( <div> <h1>Foo</h1> </div> ); }; const a = <Foo />; console.log("<Foo /> created"); export default function App() { const [count, setCount] = useState(0); const handleClick = () => { setCount((prev) => prev + 1); }; console.log("render App"); return ( <div className="App"> {count} <button onClick={handleClick}>click</button> {a} </div> ); } ``` - 如果直接將`<Foo />`填入 component 內,每次這個 component 被 re-render 的時候都會重新建立一個 React element,所以在 component 內部的 `<Foo />` 縱使長相一樣,但仍然是不同的參考 - 如果 `<Foo />` 沒有動態的 props ,則可以踢出 component 外面,但如果是有動態的 props ,則可以提升到外層,並使用 props 將 `<Foo />` 傳入,如下範例能夠做到在 `Container` 觸發 re-render 時,並不會觸發到 `Foo` 的 re-render ```js import React, { useState } from "react"; function Foo({ count }) { console.log("render Foo"); return ( <div> <h1>Foo</h1> <p>{`Foo count: ${count}`}</p> </div> ); } function Container({ content }) { const [count, setCount] = useState(0); const handleClick = () => { setCount((prev) => prev + 1); }; console.log("render Container"); return ( <div> <h1>Container</h1> <p>{`Container count: ${count}`}</p> <button onClick={handleClick}>{`Container click`}</button> {content} </div> ); } export default function App() { const [count, setCount] = useState(0); const handleClick = () => { setCount((prev) => prev + 1); }; console.log("render App"); return ( <div className="App"> <h1>App</h1> <button onClick={handleClick}>{`App click`}</button> <Container content={<Foo count={count} />} /> </div> ); } ``` - 總結兩個重點: 1. 一定要使用 React element 的方式呼叫 component function 才會被視為 component,React 判斷是不是一個 component 實例是跟呼叫方式有關,跟定義方式無關 2. 使用 React element 呼叫才能夠要求 React 去幫你呼叫,這是一種延遲呼叫的設計,也才能夠讓 React 去建立 component 實例,並且做到以下效果: - 從實例中取出資料讓 hooks 正確運作 - 可以在某些時候跳過 component function re-render Q: 要避免 children 不必要的渲染,透過 React.memo 包起來跟用 props 傳遞,這兩者有什麼區別? A: 有區別,但能做到差不多的結果,通常東西是完全固定的話就不需要使用到 memo,去做 memo 會做額外的比較,也會花效能,雖然可能不大,但如果是 props 是動態的,除了提升到外層,也可以用 memo,只是 memo 會比較所有值是否相同,有的時候提出來會影響到抽象化看起來不直覺或是 component 設計獨立性好不好呼叫的問題,但 Zet 大多數情況下不會使用 memo,除非遇到效能問題,但就結果來說,這兩種做法都可以,但沒有一定要在什麼情況下怎麼樣做。 Q: 通常用什麼方式檢查 component 渲染很多次 A: console.log 是最簡單的方法,但如果很了解這個機制,本來就會很有意地避免這個問題,但有的情況是這個東西被 re-render 也沒差,因為效能耗費不大,但有的情況是這個東西就不應該放在這裡被 re-render,這在寫的時候,如果你了解這個機制就會特別注意這個情況,或是適合放在這裡,但遇到效能問題,那就會用一些方法處理。 或是使用 React 開發工具的 [profiler](https://www.youtube.com/watch?v=Qwb-Za6cBws) 來視覺化分析哪些 component 特別花時間跟多次被 re-render。 基本習慣要養成好,但不需要過早優化,如果一開始寫就包 memo,那也不好,因為 memo 也會花額外的效能,遇到效能上的瓶頸時再來處理效能優化。 # 2024/3/31 2-8 ~ 2-9