《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] DOM, Virtual DOM 與 React element,此篇主要敘述的是《React 思維進化》 2–4 ~ 2–5 章節的筆記,若有錯誤歡迎大家回覆告訴我~
什麼是 JSX#
在上篇文章有提到,我們可透過 React.createElement 方法來建立 React element,但實際專案中卻很少看到有人用這種方式來建立 React element,而是用 JSX 語法,JSX 語法長得很像 HTML 語法,但其實 JSX 並不是在 JavaScript 中寫 HTML,接下來就要好好介紹到底什麼是 JSX ~
JSX 是 React 提供的一種語法糖,因為每次都要透過 React.createElement 來建立 React element 太麻煩啦,用 JSX 語法糖可讓開發者有類似撰寫 HTML 語法的體驗,再藉由專門的工具轉譯成 React.createElement 的呼叫語法,因此 JSX 語法本質上就是在呼叫 React.createElement 方法!只是外層用了漂亮的糖衣包裹,來提升開發者體驗。
什麼是語法糖? 語法糖是程式語言為了某些已存在的功能或語法,額外添加的便捷替代語法,但語法糖本質仍然是背後原本的功能,語法糖並沒有創造新功能,JSX 語法糖示意圖如下。

React.createElement 方法的呼叫也就是說,撰寫 JSX 語法並不是在寫 HTML 語法,他就是在呼叫 React.createElement 方法而已,只是 JSX 被刻意設計成模仿 HTML 語法的撰寫與開發體驗,讓程式碼更易於閱讀和編寫;而一段 JSX 語法其實是表達 React.createElement 方法回傳的值(也就是一個 React element)。
寫 JSX 一樣要注意 React element 屬性命名與 HTML 語法的差異(例如 class 要改寫為 className);有的學習資源會說「JSX 語法中的 class 屬性要改寫為 className」,需注意要改成 className 的原因是因為 React element 對屬性命名的規定,而不是 JSX 語法本身的要求,因為 JSX 會被轉譯成 React.createElement 的語法,因此 JSX 也要遵守對 React element 屬性的要求;也就是說,就算不寫 JSX 而是直接用 React.createElement 來建立 React element,也要遵守屬性命名轉換的規定。
以下分別用 React.createElement 語法和 JSX 語法來建立 React element,其實都是表達一樣的意思,也會得到一樣的 React element 結構。
- 以
React.createElement建立 React element
const reactElement = React.createElement( "div", { id: "wrapper", className: "foo" }, React.createElement( "ul", { id: "list-01" }, React.createElement("li", { className: "list-item" }, "item 1"), React.createElement("li", { className: "list-item" }, "item 2"), React.createElement("li", { className: "list-item" }, "item 3") ), React.createElement("button", { id: "button1" }, "I am a buton"));- 以 JSX 語法建立與上述結構一模一樣的 React element
const reactElement = ( <div id='wrapper' className='foo'> <ul id='list-01'> <li className='list-item'>item 1</li> <li className='list-item'>item 2</li> <li className='list-item'>item 3</li> </ul> <button id='button1'>I am a buton</button> </div>);以 Babel 進行 JSX 語法的轉譯#
在沒有任何處理的情況,JSX 語法在普通 JavaScript 執行環境是不合法的,會跳出如下錯誤:

因此需要專門工具來轉譯 JSX 語法,替換成真正可執行的 React.createElement 呼叫語法,而 Babel 就是可協助進行轉譯的工具。
Babel 是 JavaScript 社群中最主流的 source code transpiler,可將 JavaScript 原始碼轉譯成另一種模樣的 JavaScript 原始碼,並可透過各種 plugin 來定義轉譯範圍與效果。
補充: compiler 與 transpiler
• compiler :一種編譯工具,能將人類編寫的高階語言程式碼,轉為電腦能解讀並執行的低階機器語言執行檔。
• transpiler :一種轉譯工具,能將高階程式語言原始碼轉換成另一種模樣的高階程式語言原始碼,又稱為「source-to-source compiler」,將 TypeScript 轉譯成普通 JavaScript 程式碼,也是這種轉譯概念的應用。
負責轉譯 JSX 語法的工具稱作「JSX transformer」,JavaScript 社群中各種主流的 transpiler 幾乎都有實踐自己的 JSX transformer,而 Babel 是其中較主流也是 React 官方所推薦的,Babel 透過設置 @babel/plugin-transform-react-jsx 這個 plugin 作為 JSX transformer,就可將 JSX 語法轉為 React.createElement。
以 Babel 搭建 React 開發環境時,通常會在 Babel 引入整個 @babel/preset-react 組合包,因為 @babel/plugin-transform-react-jsx 已經被包含在 @babel/preset-react 這個組合包中,因此不需再另外引入 @babel/plugin-transform-react-jsx。若開發者使用 Create React App 或 Next.js 這種已整合好的開發環境,通常都已內建好這些設定,開發者不用再自己處理這些 plugin 的引入。
將包含 JSX 語法的檔案交給 JSX transformer 的 Babel 轉譯後,就能產出另一支 JSX 被替換成 React.createElement 的 .js 檔案,而我們在 React 專案的 HTML 所引入的 JS 檔案則是 Babel 轉譯輸出後的檔案,以確保瀏覽器要執行的程式碼不包含 JSX 語法。
需注意的是,JSX 轉譯行為是發生在開發環境的建置階段(build time),而非執行階段(runtime),照理來說當我們在開發環境修改原始碼並存檔,就該讓 Babel 重新進行一次轉譯(可用其他工具來監聽程式碼變化,一有變就自動跑轉譯),此轉譯過程稱為程式碼的靜態分析與處理,所謂「靜態」是指程式碼被實際執行之前,先以純文字形式對程式碼語意、結構等進行分析,並進行轉譯或優化等行為。
額外補充一點, Babel 是如何讀懂我們的程式碼並轉譯成 React.createElement 的呢? Babel 實際上是透過 AST 的方式來實現對程式碼的修改,AST 全名是 Abstract Syntax Tree (抽象語法樹),是一種樹狀結構,用來表示程式碼語法結構。Babel 會先將我們的程式碼轉為 AST,並透過修改 AST 來將程式碼改造成我們想要的樣子,最後再將 AST 轉為一般的程式碼輸出。關於 Babel 與 AST 可參考 透過製作 Babel-plugin 初訪 AST 。
新版 JSX transformer 與 jsx-runtime#
React 17 以前,如果在使用 JSX 語法的檔案沒有寫 import React from 'react',會在瀏覽器 runtime 遇到 React is not defined 的錯誤,因為 React 17 以前,Babel JSX transformer 會將 JSX 轉為 React.createElement() ,它預期實際執行的作用域中,已經有 React 這變數且其中也有 createElement 這方法可呼叫;如果開發者沒事先 import React from 'react',就會導致實際執行時找不到 React 這變數而產生錯誤。

import React from 'react',會在執行時噴錯而從 React 17 開始,React 官方與 Babel 合作並支援新 JSX transformer,透過新的 JSX transformer 與 React 17 開始支援的 jsx-runtime,就不需要再為了 JSX 語法而 import React。新的 JSX transformer 不再把 JSX 語法轉換成 React.createElement() ,而改成 jsx-runtime 的 _jsx 方法。

_jsx 方法可取代 React.createElement 的執行效果,即使不寫 import React,也能直接使用 JSX 語法,達到自動 import 的效果_jsx() 與 React.createElement 有何不同? 其實兩者大致相同,都是用來建立 React element 的方法,只是_jsx 方法有額外多一些優化。另外提醒,若開發者想用 JSX 以外的方法建立 React element,還是只能使用 React.createElement 語法,不應直接在原始碼中呼叫 _jsx()。
📍 因
_jsx_方法與React.createElement方法的主要用途完全互通,接下來仍以「JSX 語法會轉譯成React.createElement()語法」這說法來描述、展示範例程式碼
截至目前章節,我們瞭解了 DOM、Virtual DOM、React element 與 JSX 的概念,可用以下這張圖來總結這些概念的綜合關係。

JSX 語法規則#
JSX 語法會透過工具轉譯成React.createElement方法呼叫,為支援更多 JavaScript 的邏輯及各種資料型別的表達,JSX 會有更多的語法規則,接著就來看看有哪些語法規則吧!
嚴格標籤閉合#
在 HTML 中,有些標籤元素不會包含內容或子元素,因此不需閉合標籤,這些標籤被稱為「空元素」,如:<br>、<img>、 <input>。而另外一類本來就需要閉合的標籤,如:<p>、 <div>、 <span>,即使遺漏閉合標籤,HTML 解析器也不會出錯,因為 HTML 有容錯性,瀏覽器會根據其他內容推斷可能的結構,以建立 DOM 元素。
然而,JSX 語法是嚴格標籤閉合,即使是只寫開標籤的空元素,在 JSX 中也需寫閉合標籤,如果沒有正確將 JSX 標籤閉合,Babel JSX transformer 就會產生轉譯失敗的錯誤。
// 即使子元素為空,也一定要閉合,否則 JSX 語法會轉譯失敗const img = <img src='./image.jpg'></img>;const input = <input type='text'></input>;
// 更推薦用自我閉合的簡寫寫法const img = <img src='./image.jpg'/>;const input = <input type='text'/>;
// 沒有子元素的標籤在轉譯後會自動忽略不填第三個參數const img = React.createElement('img', {src: './image.jpg'});const input = React.createElement('input', {type: 'text'});JSX 語法的資料表達#
HTML 和 JSX 在本質上是完全不同的東西,以下說明兩者差異:
- HTML 語法:純字串格式的一段靜態文字組成的標籤語言
- 不具備運算邏輯或資料型別概念
- 無法表達除了標籤結構和固定字串外的任何資料型別,也無法表達「變數」或「運算」等表達式概念 - JSX 語法:轉譯後是可執行的 JavaScript 程式碼,非靜態 HTML 文字
- 能表達 JavaScript 所有資料型別的值
- 能直接使用變數等各種表達式
因此,若要在 JSX 表達靜態字串以外的資料型別或邏輯,就無法以 HTML 語法實現,要呈現字面值或表達式,就要選其他適當的語法。
補充:字面值與表達式
• 字面值(literal):表示固定值的表示法,不需進行額外計算。如:數字123、字串'Helo, world!'。
• 表達式(expression):計算產生值的表示法,當 JavaScript 引擎看到一個表達式,他會嘗試運算並產生一個值。表達式可包含變數、運算子、函式呼叫或另一個表達式。如:2+3是一個表達式,當 JavaScript 引擎實際執行到它,會計算出結果為5。
• 字面值和表達式的關係:兩者都是用來表示和產生值,差異在於字面值是固定的值本身,表達式是產生值的計算過程,可根據變數或函式的值變化。
在 JSX 語法中表達一段固定的字串字面值#
若想在 JSX 表達內容固定的字串字面值,因為是靜態內容,可使用和 HTML 相同的語法。
const div = ( <div id='foo' className='bar'> //以 className='bar'這種用引號把值包起來的寫法,來表達一個屬性的值是內容固定的字串字面值 這是一段字串 //子元素中若有固定的字串字面值內容,可直接寫在標籤之間 </div>)轉譯出來的內容會變成一段字串:
const div = React.createElement( 'div', { id: 'foo', className: 'bar' }, '這是一段字串');可整理成兩種使用情況:
- 若要在指定屬性值時表達字串字面值,直接用引號將值包起來,如:
className="bar" - 若要在指定子元素時表達字串字面值,直接將內容寫在開標籤與閉標籤之間
在 JSX 語法中表達一段表達式#
想表達任何「固定的字串字面值」以外的表達式時,就需要用 JSX 指定的語法 {} 來包住表達式
const number = 100;function handleButtonClick(){ alert('clicked!');}
const buttonElement = ( <button onClick={handleButtonClick}> 數字變數:{number},表達式:{number * 99} </button>)以大括號{}包起來的表達式程式碼,轉譯後會放置到對應位置:
const number = 100;function handleButtonClick() { alert("clicked!");}
const buttonElement = React.createElement( "button", { onClick: handleButtonClick }, "數字變數:", number, ',表達式:', number * 99);可整理成兩種使用情況:
- 若要在指定屬性值時表達一段表達式,用大括號
{}將表達式包起來,如:onClick={handleButtonClick} - 若要在指定子元素時表達一段表達式,一樣用大括號
{}將表達式包起來,如:<div>{children}</div>
其中,如果子元素是一段連續字串字面值,則不論字串多長、是否換行,一整段字串會被視為同一個子元素,如:'數字變數:'、',表達式:',會被視為單獨的子元素。
而如果子元素內包含表達式,則每個表達式都會被視為一個獨立的子元素,且會切分前面的字串,讓前面的字串變成獨立子元素,如:表達式 number 和 number * 99 都被視為獨立的子元素,且會切分前面的字串,如上面的範例程式碼,轉譯後的 button 元素總共有 4 個子元素,分別為 '數字變數:'、number、',表達式:'、number * 99。
將表達式視為獨立子元素的好處在於,表達式的值可能隨應用程式的互動行為而被更新,當資料連動畫面更新,進行新舊 React element 比較並只操作最小範圍 DOM element 時,獨立的子元素可更縮小操作的範圍,更降低效能成本。
在 JSX 語法中表達另一段 JSX 語法作為子元素#
「在 JSX 語法中表達另一段 JSX 語法作為子元素」意思就是在React.createElement方法中,包含另一段React.createElement方法的呼叫來作為子元素,而React.createElement 的呼叫屬於表達式,照理應該用 {} 包起來,如下:
const list = ( <ul> {/* React.createElement是一種表達式,所以用{}包起來 */} {<li>item 1</li>} 字串字面值 1 {<li>item 2</li>} 字串字面值 2 {<li>item 3</li>} </ul>)然而,HTML 語法除了固定內容字串外,另一種能表達的概念就是開與閉標籤的位置;因此 JSX 語法也支援直接在父元素的開標籤與閉標籤之間寫子元素的標籤,不須另外包 {},如下:
const list = ( <ul> {/* 直接寫在開標籤與閉標籤之間的子元素,會被 JSX transformer 自動辨識是在呼叫 React.createElement 的方法 */} <li>item 1</li> 字串字面值 1 <li>item 2</li> 字串字面值 2 <li>item 3</li> </ul>)React element 的子元素支援型別#
定義一個對應 DOM element 類型的 React element(如: div、 span)時,會因為元素類型不同而有不一樣的處理方式來轉換到實際 DOM element,以下列出各種資料型別作為 React element 子元素是如何轉換到 DOM element 的:
- React element:轉換為對應結構的實際 DOM element
- 字串值:直接印出
- 數字值:轉成字串型別後直接印出
- 布林值、
null、undefined:什麼都不印,直接被忽略而不會出現在實際 DOM element 中。方便用在條件式渲染的判斷 - 陣列:攤開成多個子元素後依序全部印出(如果陣列中每個項目的值都是可印出的型別)。有助於產生動態陣列資料對應的列表畫面
舉例來說,如果是<div>{[1,2,3]}</div>,因為子元素都是數值,數值會被轉為字串依序印出,實際 DOM element 長這樣:

<div>{[1,2,3]}</div> 轉換成實際 DOM element 的結果而如果是 <div>{[ { id: 1, itemName: "apple" }, { id: 2, itemName: "orange" }, { id: 3, itemName: "banana" } ]}</div> 則會轉換失敗,因為子元素是物件,沒辦法轉換到實際 DOM element,會出現錯誤:

- 物件、函式:無法作為子元素轉換到實際 DOM element 印出,會發生處理失敗的錯誤
畫面渲染邏輯#
畫面渲染過程中,有時會遇到需因應不同資料或狀態而變化的渲染邏輯,「動態列表渲染」和「條件式判斷渲染」是最常見情境。
動態列表渲染#
可根據陣列資料動態產生列表的畫面,React 會攤開處理陣列型別的子元素,依序渲染每個元素。
const items = \["foo", "bar"\];const element = ( <ul> <li>固定的 item</li> {/* 以原始資料的陣列產生對應的 React element 陣列 */} {/* 這段表達式會產生一個陣列,陣列中每個項目都是一個 <li> React element */} {items.map((item) => ( <li>我是 {item}</li> ))} <li>另一個固定的 item</li> </ul>);
// output.jsconst items = ["foo", "bar"];const element = React.createElement( "ul", null, React.createElement("li", null, "固定的 item"), //陣列裡的項目被依序提出,放到原本陣列的位置上,與陣列前後元素同層並排 items.map((item) => React.createElement("li", null, "我是 ", item)), React.createElement("li", null, "另一個固定的 item"));在印出 React element 陣列作為子元素時,可能會看到 React 在 console 印出一段警告:Warning: Each child in a list should have a unique 'key' prop.,因為 React 需要對陣列中的 React element 做 Virtual DOM 的效能優化處理,React 會透過 key屬性來辨別每個 React element,因此開發者需對陣列的每個 element 都填上唯一、不重複的 key 屬性。
條件式判斷渲染#
可根據資料或狀態做為條件式,來判斷是否繪製特定畫面區塊,因為 React element 只是 JavaScript 中的一種普通物件資料,可直接以 JavaScript 內建邏輯來條件式建立 React element。
const items = ["a", "b", "c"];let childElement;if (items.length >= 1) { childElement = <img src="./image.jpg" />;} else { childElement = <input type="text" name="email" />;}
const appElement = ( <div> {items.map((item) => ( <span>{item}</span> ))} {childElement} </div>);// 只是普通的 JS 邏輯來操作普通的 JS 物件資料const items = ["a", "b", "c"];let childElement;if (items.length >= 1) { childElement = React.createElement("img", { src: "./image.jpg", });} else { childElement = React.createElement("input", { type: "text", name: "email", });}const appElement = React.createElement( "div", null, items.map((item) => React.createElement("span", null, item)), childElement);透過 && 運算子來條件式渲染#
&& 適合用在「符合條件時就渲染特定畫面,不符合時則什麼都不印」的情境,&&運算子本身運算邏輯為:當運算子左邊的值為 truthy,回傳運算子右邊的值;當運算子左邊的值為 falsy,回傳運算子左邊的值。
將&&運算子應用在 JSX 語法中的範例如下:
const element = ( <div> {/* 當 isVIP 為 true 時,此表達式的值就是<h1>Hello VIP!</h1>, 當 isVIP 為 false 時,此表達式的值就是 false,不會印出任何東西 */} {isVIP && <h1>Hello VIP!</h1>} </div>)透過三元運算子來條件式渲染#
三元運算子適合用在「當條件符合時印出 A,不符合時則印出 B」的情境,三元運算子本身運算邏輯為:在條件式的值後面加一個問號(?),如果條件式為 truthy,回傳冒號(:)左邊的值,如果條件式為 falsy,回傳冒號右邊的值。
將三元運算子應用在 JSX 語法中的範例如下:
const element = ( <div> {/* 當 isLoggedIn 為 true 時,此表達式的值是 <h1>Member</h1>, 當 isLoggedIn 為 false 時,此表達式的值是 <h1>Guest</h1> */} {isLoggedIn ? <h1>Member</h1> : <h1>Guest</h1>} </div>)JSX 語法的第一層只能有一個節點#
因為一段 JSX 就是在呼叫一次 React.createElement 語法,只會回傳「一個 React element」,因此無法在 JSX 的第一層結構表達多個 React element 節點:
const element = ( //⛔這段JSX語法不合法,無法只用一個值來表達兩個 React element,transpiler 無法解析 <button>foo</button> <div>bar</div>);樹狀資料結構只能有一個根節點,若想要表達多個同層的 React element,解決辦法就是把想要放在同層的多個 React element 用一個共同的父元素包起來:
const element = ( <div> <button>foo</button> <div>bar</div> </div>);但多包一層無意義的<div> 可能導致產生多餘的層級結構而降低可讀性,因此 React 提供內建的特殊元素類型 Fragment 來建立父元素。
Fragment#
Fragment 不是對應實際 DOM element 的元素類型,也不是一種 component,而是 React 內建的特殊 React element 類型,專門處理上述情境,以 Fragment 建立 React element,實際 DOM 結構不會產生多餘元素,可將其想成一個能作為容器用途的 React element。
import { Fragment } from "react";const element = ( <Fragment> <button>foo</button> <div>bar</div> </Fragment>);另外,也可直接用空標籤來表示 Fragment 元素類型,就不用另外 import Fragment。
const element = ( <> <button>foo</button> <div>bar</div> </>);Reference:#
如有任何問題歡迎聯絡、不吝指教✍️