JSX 程式碼是如何「搖身一變」成為 DOM 的?

2023-12-05 12:00:38

JSX 是一種語法,並不是 React 中的內容,時下接入 JSX 語法的框架越來越多,但與之緣分最深的仍然是 React。本節來講一下 React 是如何搖身一變成為 DOM 的。

我們平時在寫React時會用 JSX 來描述元件的內容,例如下面的程式碼中,render 方法 return 的內容就是 JSX 程式碼。

class App extends React.Component {
  render() {
    return (
      <div className="App">
        <h1 className="title">I am the title</h1>
        <p className="content">I am the content</p>
      </div>
    );
  }
}

我們考慮以下三個問題:

  1. JSX 的本質是什麼,它和 JS 之間到底是什麼關係?
  2. React 為什麼要用 JSX?
  3. JSX 是如何對映為 DOM 的?

這一節我們就將這三個問題一一解答。

1)JSX 的本質是什麼?它和JS之間的到底是什麼關係?

JSX 到底是什麼,我們先來看看 React 官網給出的一段定義:

JSX 是 JavaScript 的一種語法擴充套件,它和模板語言很接近,但是它充分具備 JavaScript 的能力。

「語法擴充套件」這一點在理解上幾乎不會產生歧義,不過「它充分具備 JavaScript 的能力」這句,卻總讓人摸不著頭腦,JSX 和 JS 怎麼看也不像是一路人啊?這就引出了「JSX 語法是如何在 JavaScript 中生效的」這個問題。

JSX 是 JavaScript 的擴充套件,而不是 JavaScript 的某個版本,因此瀏覽器並不會天然支援,那麼 JSX 是如何在 JavaScript 中生效的呢?

React 官網是這樣的解釋的:

JSX 會被編譯為 React.createElement(), React.createElement() 將返回一個叫作「React Element」的 JS 物件。

那麼 JSX 如何轉換成 React.createElement() 的呢?答案就是通過 babel 轉換。

我們直接開啟 babel playground 來寫一段 JSX 程式碼看一下 babel 轉換後的結果。![image-20231204112041472](/Users/jiuyuezhang/Library/Application Support/typora-user-images/image-20231204112041472.png)

可以看到 JSX 程式碼都被轉換成了 React.createElement 呼叫。

接下來我們總結一下來回答標題提到的兩個問題。

JSX 是 JavaScript 的擴充套件,不是 JavaScipt 的某個版本,需要通過 Babel 進行轉換成 JavaScript 程式碼。

JSX 會被 babel 轉換為 React.CreateElement(...) 呼叫的形式,執行後返回的結果是一個物件。

2)React 為什麼要用 JSX?

從上一節我們知道 JSX 等價於一次 React.createElement 呼叫,那麼 React 官方為什麼不直接引導我們用 React.createElement 來建立元素呢?

在實際功能效果一致的前提下,JSX 程式碼層次分明、巢狀關係清晰;而 React.createElement 程式碼則給人一種非常混亂的「雜糅感」,這樣的程式碼不僅讀起來不友好,寫起來也費勁。

JSX 語法糖允許前端開發者使用我們最為熟悉的類 HTML 標籤語法來建立虛擬 DOM,在降低學習成本的同時,也提升了研發效率與研發體驗。

3)JSX 是如何對映為 DOM 的?

我們知道 JSX 經過babel轉換後會變成 React.createElement(...) 的形式,接下來我們就來一起探討一下 React.createElement(...) 是如何工作的?

3.1 入參解讀:創造一個元素需要知道哪些資訊

我們先來看看方法的入參:

export function createElement(type, config, children)

createElement 有 3 個入參,這 3 個入參囊括了 React 建立一個元素所需要知道的全部資訊。

  • type:用於標識節點的型別。它可以是類似「h1」「div」這樣的標準 HTML 標籤字串,也可以是 React 元件型別或 React fragment 型別。
  • config:以物件形式傳入,元件所有的屬性都會以鍵值對的形式儲存在 config 物件中。
  • children:子節點,如果有多個子節點,那麼依次往後寫。

舉個例子:

<ul className="list">
  <li key="1">1</li>
  <li key="2">2</li>
</ul>

經過 Babel 轉換後的形式為:

注意:從第三個入參開始往後,傳入的引數都是 children

React.createElement("ul", {
  // 傳入屬性鍵值對
  className: "list"
}, React.createElement("li", {
  key: "1"
}, "1"), React.createElement("li", {
  key: "2"
}, "2"));

3.2 出參解讀:初識虛擬DOM

下面的程式碼是 React.createElement(...) 呼叫的返回值格式。

注意:這是 fiber節點之前的每個節點的格式。

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    // REACT_ELEMENT_TYPE是一個常數,用來標識該物件是一個ReactElement
    $$typeof: REACT_ELEMENT_TYPE,

    // 內建屬性賦值
    type: type,
    key: key,
    ref: ref,
    props: props,

    // 記錄創造該元素的元件
    _owner: owner,
  };

  // 
  if (__DEV__) {
    // 這裡是一些針對 __DEV__ 環境下的處理,對於大家理解主要邏輯意義不大,此處我直接省略掉,以免混淆視聽
  }

  return element;
};

舉個例子

const AppJSX = (<div className="App">

  <h1 className="title">I am the title</h1>

  <p className="content">I am the content</p>

</div>)

console.log(AppJSX)

輸出為:

這個 ReactElement 物件範例,本質上是以 JavaScript 物件形式存在的對 DOM 的描述,也就是老生常談的「虛擬 DOM」(準確地說,是虛擬 DOM 中的一個節點

既然是「虛擬 DOM」,那就意味著和渲染到頁面上的真實 DOM 之間還有一些距離,這個「距離」,就是由大家喜聞樂見的ReactDOM.render方法來填補的。

在每一個 React 專案的入口檔案中,都少不了對 React.render 函數的呼叫。下面我簡單介紹下 ReactDOM.render 方法的入參規則:

複製程式碼

ReactDOM.render(
    // 需要渲染的元素(ReactElement)
    element, 
    // 元素掛載的目標容器(一個真實DOM)
    container,
    // 回撥函數,可選引數,可以用來處理渲染結束後的邏輯
    [callback]
)

ReactDOM.render 方法可以接收 3 個引數,其中第二個引數就是一個真實的 DOM 節點這個真實的 DOM 節點充當「容器」的角色,React 元素最終會被渲染到這個「容器」裡面去。比如,範例中的 App 元件,它對應的 render 呼叫是這樣的:

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

注意,這個真實 DOM 一定是確實存在的。比如,在 App 元件對應的 index.html 中,已經提前預置 了 id 為 root 的根節點:

<body>
    <div id="root"></div>
</body>