《React 思維進化》 筆記系列
1. [React] DOM, Virtual DOM 與 React element
2. [React] 了解 JSX 與其語法、畫面渲染技巧
3. [React] 單向資料流介紹以及 component 初探
4. [React] 認識狀態管理機制 state 與畫面更新機制 reconciliation
5. [React] 在子 component 觸發父 component 資料更新、深入理解 batch update 與 updater function
6. [React] 了解 immutable state 與 immutable update 方法
7. [React] 認識 component 的生命週期、了解每次 render 都有自己版本的資料
8. [React] React 中的副作用處理、初探 useEffect
9. [React] 對 hooks 的 dependencies 誠實,以維護資料流的連動
10. [React] React 18 effect 函式執行兩次的原因及 useEffect 常見情境
11. [React] 認識 useCallback、useMemo,了解 hooks 運作原理

前言#
承接上篇 [React] 了解 JSX 與其語法、畫面渲染技巧,此篇主要敘述的是《React 思維進化》 2–6 ~ 2–7 章節的筆記,若有錯誤歡迎大家回覆告訴我~
什麼是單向資料流#
單向資料流(one-way dataflow)是目前前端框架中蠻主流的設計模式,了解單向資料流有助於我們理解前端框架的設計理念,因此我們來看看什麼是單向資料流吧~
單向資料流是一種以資料驅動畫面的設計模式,其資料流向是:原始資料➡️模板與渲染邏輯➡️畫面結果,過程都是單向且不可逆的,只有在資料改變時才會觸發畫面的改變,畫面本身不能逆向修改資料、畫面也不會在沒更新資料時就被任意修改;單向資料流的關鍵策略是「資料與畫面分離,且維持單一的資料來源」。
而為什麼要維持這種單向的資料流向呢?限制資料更新與畫面結果的變因有助於:
- 提高可維護性:不只讓應用程式具備靈活度,讓整個應用能應變更多的情境,也讓程式碼更好維護,變因被限縮後,開發者會比較容易找到問題所在
- 提升程式碼可讀性:開發者在閱讀理解程式碼時,因為知道這是單向資料流,比較知道如何去追蹤理解程式碼,如:知道某個資料更新會連動到某模板邏輯,然後影響某個畫面的更新,只需追蹤這過程中發生的事,就可理解導致畫面更新的邏輯了
- 減少資料意外出錯的風險:因為畫面不會更改到原始資料,即使使用者不正確操作畫面也不會影響到資料
- 更好的效能優化:可準確知道畫面依據哪些資料而變化,能依據此關係做進一步的效能優化
實現單向資料流的渲染策略#
在前端開發中,通常會將資料和畫面分開處理以實現單向資料流,當資料更新後,再操作 DOM element 來讓畫面更新。以下以純 JavaScript 環境說明兩種能實現單向資料流的畫面更新策略。
策略一:資料更新後,人工判斷並手動修改所有應連動更新的 DOM element#
假設有一個 Counter 畫面,資料是 counter 的值(counterValue)和按鈕被點擊的次數(clickTimes),點擊按鈕後,counterValue的值會 +2,clickTimes的值會+1,接著更新畫面;程式碼如下:
let counterValue = 0;let clickTimes = 0;
function incrementCounterAndUpdateDOM(index) { //先更新原始資料 counterValue += 2; clickTimes += 1; // 資料更新後,需要具體知道這次資料的更新會影響到的 DOM 範圍,並且手動一一去更新: //只操作資料更新會影響的元素 document .querySelector('#counter-value > span') .textContent = counterValue;
// 修改 counter value 資料後,也需要更新點擊的總次數 //只操作資料更新會影響的元素 document .querySelector('#click-time') .textContent = clickTimes;
//補充:資料更新只會選取需要更新的 DOM 元素來更新,其他不變的元素(如:標題)則維持跟初始渲染一樣的值,不會被改動}
function initialRender() { // 只有初始化 render 時才會渲染整個畫面,包含標題和 counterValue, clickTimes document.body.innerHTML = ` <div id="counter-wrapper"> <h1>My Counter</h1> <p>點擊按鈕來讓 counter 每次都 +2</p> <p id="counter-value">counter: <span>${counterValue}<span></p> <button id="increment-btn">Clicked <span id="click-time">${clickTimes} </span> times</button> </div> `;
// increment button 事件綁定 const incrementButton = document.getElementById('increment-btn'); incrementButton.addEventListener('click', () => { // 範例行為:increment counter 和 clickTimes incrementCounterAndUpdateDOM(); });}
initialRender();過程中,被更新的資料會連動影響畫面的哪些 DOM element,需依賴開發者自行判斷(以上面範例程式碼來說,開發者須自行判斷會影響到 counter 的值和按鈕中的點擊次數文字),如何操作 DOM 的細節也要開發者手動操作(開發者要知道如何選到目標 DOM element,再更新 DOM 的內容)
此策略的優缺點:
- 優點:最小化 DOM 操作 如果開發者 DOM 操作足夠簡潔,可盡量減少多餘 DOM 操作造成的效能浪費。以上述範例來說,只更動需要變動的文字內容,其他不動,即可減少不必要的 DOM 操作
- 缺點:應用龐大時,依靠人為判斷易出錯 - 當資料更改會連動大量或複雜的畫面更新時,人為處理就容易遺漏或出錯,例如可能資料變動後需要更新數十個 DOM element,但開發者遺漏更新某些 DOM element,導致畫面結果不如預期 - 畫面有問題時,很難快速定位是哪個環節出錯,即使資料沒問題,也可能是操作 DOM element 時有錯而導致畫面結果不如預期(如上一點的例子,可能是開發者遺漏更新 DOM element,而非資料有錯),這會導致單向資料流遵循的「畫面是資料的延伸結果」已不可靠
因此,此策略完全依賴開發者的人為判斷和對 DOM 的精確操作,在大型應用中會比較不可靠、難以維護。
策略二:資料更新後,一律將整個畫面的 DOM element 全部清除,再以最新的原始資料全部重繪#
承接策略一的範例,改成在資料更新後一律重繪整個畫面,不管資料更新是影響哪些畫面元素。
當我們更新 counterValue 與 clickTimes 後,不需要知道這次資料更新會需要連動更新哪些畫面元素,而是清空整個畫面的DOM,再依據最新資料重新繪製整個畫面。
let counterValue = 0;let clickTimes = 0;
function handleIncrementButtonClick() { // 範例行為:increment counter 和 clickTimes counterValue += 2; clickTimes += 1;
// 在更新資料後,不需要判斷這次資料更新具體會影響到的 DOM elements 有哪些, // 一律呼叫 renderScreen() 來將整個畫面的 DOM elements 都清除後再全部重繪 renderScreen();}
function renderScreen() { // 每次要繪製新畫面之前,都先把整個瀏覽器畫面全部清空 document.body.innerHTML = '';
// 依據目前的最新資料,重新繪製一次整個畫面的所有 DOM elements document.body.innerHTML = ` <div id="counter-wrapper"> <h1>My Counter</h1> <p>點擊按鈕來讓 counter 每次都 +2</p> <p id="counter-value">counter: <span>${counterValue}<span></p> <button id="increment-btn">Clicked <span id="click-time">${clickTimes} </span> times</button> </div> `;
// 重新綁定 increment button 事件 document .getElementById('increment-btn') .addEventListener('click', handleIncrementButtonClick);}
renderScreen();過程中,開發者不用管資料更新的方式是什麼(例如:新增、修改或刪除),只要一律全部清空畫面,再根據最新的資料重新繪製畫面即可。
此策略的優缺點:
- 優點:開發者負擔降低 只需關注模板定義和資料更新的處理,不需手動維護資料連動到畫面的操作,實現單向資料流較直覺簡單
- 缺點:效能浪費 因為是整個畫面清除後再重新繪製,與更新的資料無關的 DOM element 也會被全部移除再重繪,當應用程式更複雜時,就會產生效能問題,影響使用者體驗
前端框架的處理策略#
上述渲染策略都有其缺點,而大多數前端框架能透過一些特殊設計來解決這些問題,讓開發者在應用這些策略時,能同時享有優點並解決缺點。Vue.js 就是採取策略一,透過追蹤資料與模板間的關係,當資料更新時,自動找到與資料變化相關的 DOM 元素並更新,可省掉開發者手動維護資料連動畫面的操作;而 React 則採取策略二,接下來就來看看 React 如何實現一律重繪渲染策略吧~
React 的一律重繪渲染策略#
在前面的章節提過,採用 Virtual DOM 的概念可優化效能,當畫面要更新時,會產生新的 Virtual DOM 畫面結構,並比較新舊的 Virtual DOM 的差異,再根據差異處操作最小範圍的實際 DOM,藉此減少效能浪費;而前面策略二又說,一律重繪策略會刪除又重建一些不需要被更新的 DOM element,而導致效能問題。
🧠!上面這兩點聽起來好像可以湊一起欸~如果一律重繪實際 DOM 會造成效能浪費,那改成一律重繪 Virtual DOM 呢?因為 Virtual DOM 只是 JavaScript 中的普通物件資料,不像實際 DOM 會和瀏覽器的一系列渲染行為綁定,重繪 Virtual DOM 會比重繪實際 DOM 更節省效能;這是個很好的想法,而 React 就是用這種方式,在一律重繪的策略二中,改用一律重繪 Virtual DOM 的方法來解決效能問題,而 React 中的 Virtual DOM 就是 React element,因此就是一律重新產生 React element~
資料更新時,React 會以新版本資料重新繪製新版的 React element,再和舊的 React element 比較,找出差異處,最後只更新這些差異處對應的實際 DOM element,示意圖如下:

因此,在 React 提及「render」通常是指「產生 React element 的流程,「re-render」則是指「重繪 React element」,以新的資料重新產生新的 React element。
延續上面範例,來看看 React 是如何渲染畫面的~
初始畫面渲染#
在第一次畫面渲染時,實際 DOM 還沒有任何元素,React 會將完整的 React element 結構對應到實際 DOM element 上,此時的 React element 會是第一個版本。
// 原始資料 counterValue 為 0,clickTimes 為 0const reactElement1 = ( <> <div id="counter-wrapper"> <h1>My Counter</h1> <p>點擊按鈕來讓 counter 每次都 +2</p> {/* counterValue 的值 */} <p id="counter-value">counter: <span>0<span></p> {/* clickTimes 的值 */} <button id="increment-btn">Clicked <span id="click-time"> 0 </span> times</button> </div> </>);更新畫面的渲染#
當使用者點擊一次按鈕時,counterValue 被更新為 2,clickTimes 被更新為 1,React 不會修改舊的 React element(記得之前提過,React element 在建立後是不可被事後修改的嗎?),而是以新版資料再次產生一個完整畫面的、新的 React element:
// 原始資料 counterValue 為 2,clickTimes 為 1const reactElement2 = (
<> <div id="counter-wrapper"> <h1>My Counter</h1> <p>點擊按鈕來讓 counter 每次都 +2</p> {/* counterValue 的值 */} <p id="counter-value">counter: <span>2<span></p> {/* clickTimes 的值 */} <button id="increment-btn">Clicked <span id="click-time"> 1 </span> times</button> </div> </>);產生新的 React element 後,會和舊的 React element 比較並找出差異處,並只更新這些差異處對應的實際 DOM element,其餘皆不動。

📑 React 各種觀念與核心機制幾乎都是為了實現單向資料流,畫面管理與資料機制都圍繞這策略而打造;了解此策略有助於之後學習更多 React 的管理機制哦~
畫面組裝的藍圖:component 初探#
什麼是 component#
component(元件)是一段可重用的程式碼,這段程式碼是開發者自定義的畫面元件藍圖,負責處理特定範圍的畫面內容或邏輯。可透過不同 component 來組合出整個畫面,如:sidebar、menu、footer 等,component 內也可包含 component,嵌套後組合成更複雜的 component。
通常我們會根據商業邏輯或重用性,來設計一個自定義的 component。例如一個商品列表的畫面,在列表區塊會包含數個商品品項的子區塊,及分頁控制項,就可分為下列 component:
- ProductList:最外層的列表,包含 ProductListItem 和 Pagination 兩種子 component
- ProductListItem:每一個商品項目的區塊,商品資訊不同,但樣式與排版相同、可重用
- Pagination:將分頁控制抽象化,讓其他列表也可重用分頁功能
拆分元件的好處是什麼?拆分元件有助於提高程式碼可重用性,也讓維護與管理程式碼更有效率,例如如果設計師說商品項目的樣式要更改,開發者只需要更改 ProductListItem 這個 component 的樣式即可,不需要更改太多地方,就可讓整體樣式一起被更改,讓維護、修改程式碼變得更有效率。
定義 component#
React component 可透過 JavaScript 的函式來定義,此函式接收開發者自訂格式的 props (properties,屬性) 資料作為參數,並回傳一個 React element 作為畫面結構區塊。component 的名稱可自定義,但 component function 名稱的首字母必須大寫。
export default function MyTitle(props){ return ( //這裡的 JSX 語法在轉譯後是 React element 方法的呼叫,所以這 function 回傳的是一個 React element <h1>I'm a title</h1> )}一個定義好的 component 是記載「一段產生特定結構 React element 的流程」,也就是紀錄如何產生一段 React element,例如:如何得到需要的值、如何運算或判斷要產生哪段 React element 等等,這些邏輯會依據需求一併被封裝進元件內。而函式適合用來定義一段邏輯或流程,是 React 可以用函式來定義 component 的原因之一。
補充:React component 可以用普通 JavaScript 函式來定義,也可用 class 語法來定義。React 16.8 後,function component 搭配 hooks 逐漸取代傳統的 class component,成為主流選擇;因此新開發者建議直接學 function component 搭配 hooks
呼叫 component#
自定義的 component 可透過 React element 的形式被呼叫:
const reactElement = <MyTitle />//JSX 會被轉譯成:const reactElement = React.createElement(MyTitle)當React.createElement 第一個參數(元素類型)是 component function 時,就能建立這個 component 定義的 React element。
React 看到 component function 後,就會執行 component function,再將其回傳的畫面區塊放置到原本呼叫自定義 component 的地方,最後再和其他區塊一起被轉為實際的 DOM element。
function MyTitle(props){ return ( <h1\>I'm a title</h1\> )}
//MyTitle 被呼叫三次,component function 就會被執行三次//最後將 component function 回傳的 React element 放到 <MyTitle /> 放置的位子上const reactElement = ( <ul> <li> <MyTitle /> </li> <li> <MyTitle /> </li> <li> <MyTitle /> </li> </ul>);藍圖與實例#
進一步說明 component function 與呼叫 component 的差異:
- component function:是一份描述特徵、流程與行為的「藍圖」,是特定畫面的產生流程與邏輯,不是已經產生好的畫面。(有點類似:欸我預計要這麼做!這是我的規劃啦,但我還沒做XD)
- 呼叫 component:產生實際的「實例」,呼叫 component 以後,就會根據藍圖實際產出東西,但產出的東西彼此之間是獨立存在、互不影響的
舉例來說,component function 就像是一份如何製作抹茶巴斯克的食譜或配方,是一份藍圖,告訴你可以按照這流程製作,而我們可以按照這食譜製作很多份相同的抹茶巴斯克,也就是產出實際的「實例」,但每一份抹茶巴斯克都是獨立的,不會吃了抹茶巴斯克 A 而導致抹茶巴斯克 B 也變少了;而 component function 是一份配方,代表實際製作時我們可以根據需求、客製化產出不同甜度的抹茶巴斯克。
上述範例的 MyTitle 這個 component function 就是一份「如何產出特定模樣的標題」的藍圖,而以 <MyTitle /> 呼叫 component 時則是實際產出實例,建立 React element。
補充:藍圖與實例 當我們在程式設計中,用一個函式或模板描述某種特徵、處理流程或行為時,會稱這函式是一種「藍圖」;而根據藍圖產生的實際個體,稱為該藍圖的「實例」,每個實例都有獨立狀態,不受其他實例影響。
Import 與 export component#
實際開發時,通常會將 component function 定義在獨立檔案中,並搭配 ES module 語法,依據匯出方式不同,會有對應的匯入語法。
匯出的方式分為 default export 與 named export:
default export:一個檔案中只能有一個 default export#
function MyComponent() { //component 內容}export default MyComponentnamed export:一個檔案中可以有多個 named export#
export function ComponentA() { //component 內容}export function ComponentB() { //component 內容}匯入的方式則依據匯出的方式而定:
如果是用 default export#
import MyComponent from './MyComponent'如果是用 named export#
import { ComponentA, ComponentB } from './MyComponents'在慣例上通常會讓一個檔案有一個主要的 component 來作為 default export,且檔案命名與 component 名稱相同。
Props#
什麼是 props ?「props(properties)」這個機制可讓我們將特定參數從外部傳給 component,讓 component 可根據參數進行客製化的流程或邏輯,讓 component 更有彈性。React component 的 props 抽取我們關注的特性,並封裝其他實作細節在 component 中,當我們使用 component 時,只須根據需求傳遞不同 props,就能有效重用,而不用知道 component 內部的實現細節。
如何傳入 props? 可在呼叫 component 時傳入 props,這些 props 會自動被打包成一個 JavaScript 物件,實際執行 component function 時此物件會作為函式參數傳入。
// 以下是呼叫 component 時傳入 props 的範例// 調用兩次 ProductListItem component,但傳入不同 props,以呈現不同資訊const reactElement = ( <div> <ProductListItem title="Book" price={180} imageUrl="./book.jpg" /> <ProductListItem title="pencil" price={100} imageUrl="./pencil.jpg" /> <div/>)React 沒有限制 component 的 props 可以傳遞什麼樣的資料型別,一切由開發者自行決定,基本資料型別、物件、陣列、函式、React element(其本身只是個 JavaScript 物件)都可作為 props 的值。
接收與使用 props#
component function 接收的第一個參數是 props 物件,此物件會包含調用 component 時傳入的各種值,可直接取用 props 物件,或用物件解構的方式取得 props 內容。
- 取得 props 物件
export default function ProductListItem(props){ return ( <div> <h2>{props.title}</h2> <img src={props.imageUrl}/> <p>${props.price}</p> </div> )}- 以物件解構的方式取得內容,習慣上更建議這樣做,會讓開發更有效率
export default function ProductListItem({titile, price, imageUrl}){ return ( <div> <h2>{title}</h2> <img src={imageUrl}/> <p>${price}</p> </div> )}Props 唯獨、不可被修改#
React 的 props 是唯獨的,目的是維護單向資料流可靠性,讓資料源頭保持不變以便追蹤。如果在 component 內部隨意修改 props 會導致無法找到資料改變的源頭、難以預測原始資料與畫面結果的因果關係、難以追蹤導致畫面錯誤的根源等等非預期性錯誤。
開發環境的 React 會以 Object.freeze(props) 來將 props 物件凍結起來,以避免開發者不安全的修改 props。
export default function ProductListItem(props){ //⛔️ 這是錯誤示範! 請不要這樣修改 props! // 開發環境的 React 會讓你的 props 修改無法產生效果 props.price = props.price \* 0.9;
return ( <div> <h2>{props.title}</h2> <img src={props.imageUrl}/> <p>${props.price}</p> </div> )}如果需要以 props 的值做其他運算,可創建新變數,將依據 props 的值計算而產生的值儲存在新變數。
export default function ProductListItem(props){ //✅ 另外用新變數存放運算結果 const discountPrice = props.price * 0.9;
return ( <div> <h2>{props.title}</h2> <img src={props.imageUrl}/> <p>${props.price}</p> </div> )}⚠️ 不過 React 針對 props 做的 Object.freeze(props) 輔助阻擋仍有其限制,有時候 React 還是無法偵測到 props 被更改,而導致畫面產生錯誤行為,因此還是需要開發者自己有意識地去避免踩雷。
特殊的 props:children#
children prop 賦值方式和其他 prop 不同,會在呼叫 component 時將 children prop 的值寫在開標籤和閉標籤之間,像這樣:<MyComponent>你好,我是 children prop 的值</MyComponent>。
在 component 內,可透過 props.children 將值取出:
export default function MyComponent(props){ return <div>{props.children}</div>}children prop 常見用途是用來設計「畫面容器」類型的 component,由component 提供容器的結構或樣式,但不寫死容器內的具體內容,具體容器內容是呼叫 component 時再由外部透過 props 傳入。例如我們想要有個有圓角、淺灰底的卡片樣式,但卡片內容是什麼則由呼叫時決定,就可以這樣寫:
function Card(props) { //透過 .card 定義Card的樣式,但容器內容由外部提供 return <div className="card">{props.children}</div>}
function App() { return ( <Card> <h1>Hello React!</h1> <p>I am learning React</p> </Card> )}在上篇文章有提到「React element 的子元素支援型別」,要定義一個對應實際 DOM element 類型的 React element 時,其子元素只能是特定型別,因為子元素會轉為實際的 DOM element,像函式或物件就不可作為子元素(也就是不可作為 children prop),例如: <div>{{ id: 1, name: "foo" }}</div>這樣寫是不行的,物件{ id: 1, name: "foo" }沒辦法轉為實際 DOM element 渲染在畫面上。
⬆️這裡之前筆誤,
<div>{[1,2,3]}</div>是可以渲染在畫面上的哦,因為陣列子元素都是數值,數值可被轉為字串直接印出,更多說明請見這篇
然而,component 類型的 React element 的 children prop 沒有類型限制,可填入任何型別,因為自定義 component 的 children prop 會如何應用由開發者自行決定,不一定會用在渲染畫面元素,舉例如下:
// 傳入一個函式作為 MyButton 的 children// MyButton 內沒有將 children 渲染在畫面上,而是作為事件綁定的函式// 補充:不過 children prop 還是較常用在容器類型的 component,例如上面 Card 的例子function MyButton(props) { return <button onClick={props.children}>click meeee</button>}
function App() { return <MyButton>{() => console.log('Hello React!')}</MyButton>}由此可看出,children prop 和其他一般 props 一樣,沒有類型限制,children 除了傳值方式較特別,其他特性其實都和一般的 props 特性相同~
父 component 與子 component#
component 內可呼叫其他 component 作為子 component,可藉此來組裝較複雜的畫面。以一個常見的商品列表為例,我們可分為 ProductListItem、ProductList、App 三個 component,他們的嵌套關係如下:
ProductListItem:根據傳入的 props 顯示商品詳細資訊#
export default function ProductListItem({ title, price, discountRange, imageUrl,}) { return ( <div className="product-list-item"> <h2>{title}</h2> <p>價格:{price}</p> {/* 如果 discountRange 存在,才會顯示 price * discountRange 的折扣價格 */} {Boolean(discountRange) && <p>折扣價:{price * discountRange}</p>} <img src={imageUrl} /> </div> );}ProductList:可根據「熱門商品」資料或「特價商品」資料(藉由傳入的 products prop 資料來分辨)來顯示不同列表#
import ProductListItem from './ProductListItem'export default function ProductList({ products }) { return ( <div className="product-list"> {products.map((product) => ( <ProductListItem key={product.id} title={product.title} price={product.price} discountRange={product.discountRange} imageUrl={product.imageUrl} /> ))} </div> )}App:傳入不同的商品列表資料給 ProductList,分別展示「熱門商品」和「特價商品」#
import ProductList from './ProductList'const popularProducts = [ { id: 1, titile: 'popularItem1', price: 1200, imageUrl: '1.jpg' }, { id: 2, titile: 'popularItem2', price: 800, imageUrl: '2.jpg' },]const onSaleProducts = [ { id: 3, titile: 'onSaleItem1', price: 600, discountRange: 0.8, imageUrl: '3.jpg', }, { id: 4, titile: 'onSaleItem2', price: 400, discountRange: 0.7, imageUrl: '4.jpg', },]
export default function App() { return ( <div className="App"> <h1>熱門商品</h1> <ProductList products={popularProducts} />
<h1>特價商品</h1> <ProductList products={onSaleProducts} /> </div> )}從上述例子可看出,拆分 component 可讓每個檔案、每個 component 的職責更明確且單一,component 有各自的任務,組合後就可渲染完整畫面,可看出這樣的拆分有助於提高程式碼可讀性和可重用性。
Component 的 render 與 re-render#
當我們以<ComponentName /> 呼叫 component 時,React 在繪製畫面時就會執行 component function 內的邏輯,並在最後回傳一段 React element,而這過程就稱為 component 的「一次 render」。
如果 component function 回傳的 React element 有呼叫其他 component 作為子 component,就會接著觸發子 component 的一次 render,層層往下觸發,直到不再遇到子 component 為止,最後組成可對應實際 DOM element 的 React element 結構。
大概的流程是這樣:
- 呼叫父 component
- 執行父 component 的一次 render
- 父 component 回傳 React element
- 發現回傳的 React element 包含子 component
- 觸發執行子 component 的一次 render
- 回傳子 component 的 React element
- 確認此 React element 內沒有其他子 componet (如果有就繼續觸發子 component 渲染)
- 將子 component 回傳的 React element 組裝進父 component 的 React element
- 組裝成可對應實際 DOM element 的 React element 樹狀結構
- 以這結構產生或更新實際 DOM
接下來以包含多層 component 的範例來說明 render 的過程:
function MyComponent1() { console.log('render MyComponent1') return ( <div className="MyComponent1-wrapper"> <h1>I am MyComponent1</h1> <MyComponent2 /> <MyComponent2 /> </div> )}
function MyComponent2() { console.log('render MyComponent2') return ( <div className="MyComponent2-wrapper"> <h2>I am MyComponent2</h2> <MyComponent3 /> <MyComponent3 /> </div> )}
function MyComponent3() { console.log('render MyComponent3') return ( <div className="MyComponent3-wrapper"> <h3>I am MyComponent3</h3> </div> )}
export default function App() { return <MyComponent1 />}可從 console 來看 render 的順序:

整個 render 流程由上至下、由外至內,React 會在每個子 component render 完成後,再繼續處理下一個 component,呼叫流程示意圖如下:

從一律重繪策略到 component 的 re-render#
當 component function 首次呼叫並執行時,會執行第一次 render,產生初始狀態畫面,而當 component 內部狀態更新時,React 會再次執行 component function 以產生新版本的畫面,稱為「re-render」,re-render只是重新產生 React element,而非操作實際 DOM,因此不會直接連動瀏覽器渲染引擎的渲染。
為什麼 component 命名中的首字母必須為大寫?#
因為 transpiler 會透過標籤的首字母大小寫來判斷這是對應實際 DOM element,還是對應自定義 component。
當標籤類型名稱首字母為小寫,例如<p> ,transpiler 會將 p 視為字串,轉譯成 React.createElement('p');;當標籤類型名稱首字母為大寫,例如<MyTitle> ,transpiler 會將MyTitle視為變數,轉譯成 React.createElement(MyTitle);。
另外,自定義 component 命名的首字母若使用大寫,能讓開發者更方便判斷是否為自定義 component,對開發體驗也比較好。
最後,補充作者 Zet 提出的問題:「component 這東西設計的本質與意義是什麼?」
此問題核心關鍵是「依據需求及邏輯意義進行抽象化」,開發者根據需求將關心的特徵、行為歸納出來後,設計一套適用於特定情境和範圍的流程,並將實作細節封裝起來以便重用,這一套被封裝起來的東西就是 component;也因此設計 component 時我們應該思考:這個 component 預計服務的情境有哪些? 表達的意義範圍邊界到哪? 再根據這些方向設計 props 與資料流、或拆分更多 component…等。
如有任何問題歡迎聯絡、不吝指教✍️