[React] 了解 JSX 與其語法、畫面渲染技巧

Monica
24 min readMar 2, 2024

--

《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 語法糖示意圖如下。

撰寫 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 這變數而產生錯誤。

React 17 以前,如果在使用 JSX 語法的檔案沒有寫 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 方法。

React 17 開始,新的_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 相同的語法。

// input.js
const div = (
<div id='foo' className='bar'> //以 className='bar'這種用引號把值包起來的寫法,來表達一個屬性的值是內容固定的字串字面值
這是一段字串 //子元素中若有固定的字串字面值內容,可直接寫在標籤之間
</div>
)

轉譯出來的內容會變成一段字串:

// output.js
const div = React.createElement(
'div',
{
id: 'foo',
className: 'bar'
},
'這是一段字串'
);

可整理成兩種使用情況:

  • 若要在指定屬性值時表達字串字面值,直接用引號將值包起來,如:className=”bar”
  • 若要在指定子元素時表達字串字面值,直接將內容寫在開標籤與閉標籤之間

在 JSX 語法中表達一段表達式
想表達任何「固定的字串字面值」以外的表達式時,就需要用 JSX 指定的語法 {} 來包住表達式

//input.js
const number = 100;
function handleButtonClick(){
alert('clicked!');
}

const buttonElement = (
<button onClick={handleButtonClick}>
數字變數:{number},表達式:{number * 99}
</button>
)

以大括號{}包起來的表達式程式碼,轉譯後會放置到對應位置:

//output.js
const number = 100;
function handleButtonClick() {
alert("clicked!");
}

const buttonElement = React.createElement(
"button",
{ onClick: handleButtonClick },
"數字變數:",
number,
',表達式:',
number * 99
);

可整理成兩種使用情況:

  • 若要在指定屬性值時表達一段表達式,用大括號{}將表達式包起來,如:onClick={handleButtonClick}
  • 若要在指定子元素時表達一段表達式,一樣用大括號{}將表達式包起來,如:<div>{children}</div>

其中,如果子元素是一段連續字串字面值,則不論字串多長、是否換行,一整段字串會被視為同一個子元素,如:'數字變數:'',表達式:',會被視為單獨的子元素。

而如果子元素內包含表達式,則每個表達式都會被視為一個獨立的子元素,且會切分前面的字串,讓前面的字串變成獨立子元素,如:表達式 numbernumber * 99 都被視為獨立的子元素,且會切分前面的字串,如上面的範例程式碼,轉譯後的 button 元素總共有 4 個子元素,分別為 '數字變數:'number',表達式:'number * 99

將表達式視為獨立子元素的好處在於,表達式的值可能隨應用程式的互動行為而被更新,當資料連動畫面更新,進行新舊 React element 比較並只操作最小範圍 DOM element 時,獨立的子元素可更縮小操作的範圍,更降低效能成本。

在 JSX 語法中表達另一段 JSX 語法作為子元素
「在 JSX 語法中表達另一段 JSX 語法作為子元素」意思就是在React.createElement方法中,包含另一段React.createElement方法的呼叫來作為子元素,而React.createElement 的呼叫屬於表達式,照理應該用 {} 包起來,如下:

// input.js
const list = (
<ul>
{/* React.createElement是一種表達式,所以用{}包起來 */}
{<li>item 1</li>}
字串字面值 1
{<li>item 2</li>}
字串字面值 2
{<li>item 3</li>}
</ul>
)

然而,HTML 語法除了固定內容字串外,另一種能表達的概念就是開與閉標籤的位置;因此 JSX 語法也支援直接在父元素的開標籤與閉標籤之間寫子元素的標籤,不須另外包 {},如下:

// input.js
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(如: divspan)時,會因為元素類型不同而有不一樣的處理方式來轉換到實際 DOM element,以下列出各種資料型別作為 React element 子元素是如何轉換到 DOM element 的:

  • React element:轉換為對應結構的實際 DOM element
  • 字串值:直接印出
  • 數字值:轉成字串型別後直接印出
  • 布林值、nullundefined:什麼都不印,直接被忽略而不會出現在實際 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,就會處理失敗
  • 物件、函式:無法作為子元素轉換到實際 DOM element 印出,會發生處理失敗的錯誤

畫面渲染邏輯

畫面渲染過程中,有時會遇到需因應不同資料或狀態而變化的渲染邏輯,「動態列表渲染」和「條件式判斷渲染」是最常見情境。

動態列表渲染

可根據陣列資料動態產生列表的畫面,React 會攤開處理陣列型別的子元素,依序渲染每個元素。

// input.js
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.js
const 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。

// input.js
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>
);

// output.js
// 只是普通的 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:

如有任何問題歡迎聯絡、不吝指教✍️

--

--