【React 學習】JSX 初探

2023/12/19閱讀時間約 18 分鐘
圖片來源:ProgrammerHumor.io

圖片來源:ProgrammerHumor.io


JSX 全名為 JavaScript syntax extention,是一種 JavaScript 的語法擴充,也有人說是一種語法糖 🍬 JSX 讓我們能在 JavaScript 裡面撰寫近似於 HTML 的 markup。雖然 JSX 和 React 並非絕對要一起使用,但大多數開法選擇 React 當作前端框架後,也都會引入 JSX。

本篇學習筆記將討論:

  • JSX 想解決的問題
  • JSX 撰寫注意事項
  • React 背後如何處理 JSX



為何使用 JSX?

初來乍到,你可能會覺得 React 很煩,沒事幹嘛把 markup 和渲染邏輯湊在一起。但請回想一下 React 的宗旨:將 UI 切割成不同元件組合起來。既然都切割成不同元素了,那把按鈕的 HTML markup 以及渲染邏輯放作伙,似乎也合情合理,確保 UI 渲染更加即時。

過往我們都把 HTML、CSS、JavaScript 的程式碼放在各自檔案裡,現在導入元件的概念後,我們希望不同的元件不要並存在同個檔案,方便管理與維護。但總不能把 HTML 放在 JavaScript 檔案,所以才衍生出了 JSX。

JSX 看起來像 HTML,但比 HTML 還要更加嚴格,而且可以接受動態資訊,比方說讓某個 <h1> 呈現出 JavaScript 變數的字串值等等,就像學校裡以嚴厲出名,卻能激發學生潛能的黑臉教師。



JSX 的注意事項

前面提到 JSX 比 HTML 還要嚴格,自然多了一些需要注意的規則。一旦沒有遵循規則,React 應用程式的畫面會跳出巨大錯誤訊息,跟著提示修正基本上不會有大問題,但能避免踩雷就避開吧。


👩‍🏫 回傳單一根元素

一個 React 元件 (component) 只能回傳一個元素 (element)。以下面程式碼為例,TodoList 這個元件想要回傳 <h1><img><ul>,實在非常貪婪。

// ☣️ This does not work!!!
export default function TodoList() {
return (
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
/>
<ul>
<li>Invent new traffic lights</li>
<li>Rehearse a movie scene</li>
<li>Improve the spectrum technology</li>
</ul>
);
}


若希望回傳多個元素,JSX 提供以下兩種撰寫方式:

  1. 用一個父元素把要回傳的元素們包起來,像大家熟悉的 div
  2. 或是用 <> </> 把元素們包起來,這樣的好處是減少無謂的 HTML。這樣的標記稱為 fragment,不會在瀏覽器的 HTML tree 留下痕跡
<>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
/>
<ul>
...
</ul>
</>


看到這邊,我們不禁想問,為什麼 JSX 一定要把多個元素包起來才能回傳呢?這就要再次釐清 JSX 只是 JavaScript 的語法擴充,所以最終還是要轉換成 JavaScript 物件的。想想以前寫 JavaScript 時,我們不會在函式中回傳多個物件,而是把物件們包在另一個物件或陣列中。所以別哭了,乖乖包起來就對了。


👩‍🏫 關閉所有標籤

HTML 的 self-closing 例如 img 沒有強制關閉,但 JSX 所有標籤都需要閉闔起來。也就說,<img src="..." alt="..."> 這樣的寫法違規,請改成 <img src="..." alt="..." />。注意最後面的閉合符號 /


👩‍🏫 駝峰式命名

前面提過 JSX 最終要轉換成 JavaScript 物件,所以元素中的屬性 (attribute) 會變成物件的 key。但而在 React 元件中,我們時常把屬性讀到變數中。好死不死,JavaScript 對於變數的命名有所限制,例如 class 即為無法使用的保留關鍵詞,所以善用駝峰式命名能規避掉這個問題。

<img 
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo" // Use camelCase 🐪
/>



在 JSX 中帶入 JavaScript

雖說 JSX 是 JavaScript 的語法擴充,但若是要帶入 JavaScript 邏輯或動態資訊到裡面,還是需要經過特別手續處理的,可謂是最熟悉的陌生人。而這道特別手續也不難,就是使用 {...}。相信之前使用過 Handlebars 或是 Ejs 等樣板引擎都不會太陌生。

舉個例子,要把圖片來源和 alt 利用動態的方式傳入 JSX 中,可以這麼做:

export default function Avatar() {
const avatar = 'https://i.imgur.com/7vQD0fPs.jpg';
const description = 'Gregorio Y. Zara';
return (
<img
className="avatar"
src={avatar} // Use curly braces
alt={description} // Use curly braces
/>
);
}


利用大括號傳入 JS 物件

舉例來說,我們有個名為 person 的物件,一樣可以用 dot notation 來帶入物件值,但記得外圍用 { ... } 包覆:

const person = {
name: 'Gregorio Y. Zara',
theme: {
backgroundColor: 'black',
color: 'pink'
}
};

export default function TodoList() {
return (
<div style={person.theme}> // Pass object data
<h1>{person.name}'s Todos</h1> // Pass object data
<img
className="avatar"
src="https://i.imgur.com/7vQD0fPs.jpg"
alt="Gregorio Y. Zara"
/>
<ul>
<li>Improve the videophone</li>
<li>Prepare aeronautics lectures</li>
<li>Work on the alcohol-fuelled engine</li>
</ul>
</div>
);
}



React JSX 背後如何運作?

JSX 身為擴充語法 / 語法糖,瀏覽器自然讀不懂,所以我們需要轉譯器 (Babel 或 Typescript 等等) 將 JSX 轉譯成瀏覽器理解的語言,就是翻譯蒟蒻啦。

💡 Babel 是巴別塔的意思,著名小說《巴別塔學院》聽說非常厲害。


React 17 之前

轉譯器會將 JSX 先轉成 React.createElement() 的函式呼叫,所以我們需要用 import 關鍵字來引入 react api。

import React from 'react'
function Greet(){
return <h1>Hello World!</h1>
}


上面的程式碼經過轉譯後會變成這樣。Greet 元件渲染 h1,其實背後是呼叫 React.createElement() 這個函式。所以上面的程式碼在瀏覽器接收前,會先被轉成 :

import React from 'react'
function Greet() {
return React.createElement("h1", {}, "Hello, World!")
}


根據 React 的官方文件,createElement 提供三個參數:

  • type:必須是有效的 React 元件類型,比方說 HTML tag 的名稱字串 ("div""span" 等等),或是 React 元件 (函式、fragment等等)
  • prop:必須是物件或是 null。如果傳入 null,將被視為空物件。React 會根據 prop 參數,建立一個 prop 對應的元素 (繞口令)
  • children:非必要,子層元素。
React.createElement(
type,
[props],
[...children]
)


假設我們用 JSX 寫出以下元件:

import React from 'react'

function App (){
return (
<div>
<p>This is a list</p>
<ul>
<li>List item 1</li>
<li>List item 2</li>
</ul>
</div>
);
};


會被轉譯成這樣,React.createElement() 的 ​[...children] 參數會再傳入子層元素呼叫的 createElement()...... 事實上,這就是不用 JSX 的純 React 寫法,如果子層不斷累加,易讀性將逐漸失控,因此大多數人才選擇採用 JSX。

import React from 'react'

// Without JSX. Pure React only...​
function App() {
return React.createElement(
"div",
null,
React.createElement("p", null, "This is a list"),
React.createElement(
"ul",
null,
React.createElement("li", null, "List item 1"),
React.createElement("li", null, "List item 2")));
}


我們先前提過,JSX 最終會被轉成 JavaScript 物件,而這正是 React.createElement 的工作,這些 JavaScript 物件也被稱作 react element。我們可以把它想像成是給 React 的渲染指示。

{
"type": "div",
"key": null,
"ref": null,
"props": {
"children": [
{
"type": "p",
"key": null,
"ref": null,
"props": {
"children": "This is a list"
},
"_owner": null
},
{
"type": "ul",
"key": null,
"ref": null,
"props": {
"children": [
{
"type": "li",
"props": {
"children": "List item 1"
},
// truncated for brevity
},
{
"type": "li",
"props": {
"children": "List item 2"
},
// truncated for brevity
}
]
},
"_owner": null
}
]
},
"_owner": null
}


要注意這些 react element 雖然在某種層面上代表我們所期待的 HTML,但它們並非存在於真正的 DOM,而是在虛擬 DOM (virtual DOM),React 讀取這些物件之後,會在虛擬 DOM 建立對應的 HTML 元素。

虛擬 DOM 又是什麼鬼?簡單來說,虛擬 DOM 是真實 DOM 的輕量級複製品。由於直接操作 DOM 曠日廢時,所以 React 使用了虛擬 DOM 技術來加快速度。

由於虛擬 DOM 並沒有真正渲染到畫面上,所以每當 state (資料) 更動時:

  1. 先比較新虛擬 DOM 和舊虛擬 DOM
  2. 找出更新的部分,以下圖為例,就是新增的 <h3>
  3. React 透過批量更新 (batch updates),只更新真實 DOM 必要的部分
  4. 節省 UI 重新渲染的時間



React 17 後的 JSX 背後運作

新的 JSX 轉換模式,改為直接從 react 套件引入特殊的函式並呼叫它們,不用再把 JSX 先轉成 React.createElement() 了,所以也不需要在頂部引入 react api。但是,若要使用 React Hook 的話,還是要引入!

要了解這次改變,我覺得直接去 Babel 的官網 repl 查看最直接:

raw-image


阿呀呀,果然沒有出現 createElement() 呢,import {jsx as _jsx} from 'react/jsx-runtime' 也並非我們手動引入,而是轉譯器為之。但如果要手動建立 react element 的話,還是可以繼續使用老朋友 React.createElement(),有的時候,舊愛最美。



結語

一直以來我對於轉譯的過程都很有興趣,也許因為之前當過翻譯的關係?以往我們常常只關注 JSX 語法和規則。這次,間接認識到了像虛擬 DOM 和 React.createElement() 等等。雖然或許無法在開發或工作方面有直接幫助,但使用工具時若能先了解背後運作方式,也是滿不錯的事情~


參考資料:









    16會員
    34內容數
    Bonjour à tous,我本身是法文系畢業,這邊會刊登純文組學習網頁開發的筆記。如果能鼓勵更多文組夥伴一起學習,那就太開心了~
    留言0
    查看全部
    發表第一個留言支持創作者!