「深入淺出」實現 JSX 的轉換

前言

由於近期在看React框架源碼、底層實現方面的知識,所以想把學習心得整理出來。

這也是一個新的系列**「從 0 實現 React 18 核心模塊」**的第一篇。

接下來還會更新:render、commit 階段的實現,以及 Hooks 架構、useState、useEffect、單雙節點 Diff 的過程還有 React 18 中的併發更新原理。

在看文章之前,我們可以先想幾個問題:

下文提到的 big-react 是從 0 到 1 實現的 React 的核心功能模塊原理的項目

如果自己實現一個 React 框架,它需要包含哪些內置的包:

如果還有一個必要的包,那就是react-dom

react 與 react-reconciler 包是什麼

react 包爲我們提供了什麼

當我們在項目中使用 React 構建界面時,主要使用的就是 react 包。它提供了開發者需要的所有 API。如React.ComponentReact.createElementReact.useState等等,所以它也是大多數 React 項目的基礎。

react-reconciler 包實現了什麼

react-reconciler包是一個更底層、更高級的庫,它實現了reconciliation協調算法,reconciliation是 React 的一種核心優化策略,用於在更新組件時比較虛擬 DOM 樹的差異,並將實際更改應用到實際的 DOM 樹。這有助於提高性能,因爲避免了不必要的 DOM 操作。

它主要用於創建自定義渲染器,以及在不同的平臺中去使用 React。例如,react-dom(用於 Web 平臺)和 react-native(用於移動應用)都使用 react-reconciler 作爲底層庫,實現了針對各自平臺的渲染邏輯。

JSX 是什麼

const element = <div class>Hello, world!</div>;

在 React 中,JSX 是一種 JavaScript 語法擴展,允許你在 JavaScript 代碼中編寫類似 HTML 的標記。要使用 JSX,需要在構建過程中將其轉換爲標準的 JavaScript 代碼。

通常,這個轉換過程包括兩個主要部分:

JSX 被 Babel 編譯成了什麼

在 React 17 之前,JSX 語法會被編譯成React.createElement函數的調用,用來創建虛擬 DOM 元素。

轉換結果如下:

const element = React.createElement(
  "div",
  { className: "container" },
  "Hello, world!"
);

從 React 17 開始,引入了新的 JSX 轉換功能,稱爲 "Runtime Automatic"(自動運行時)。這意味着在使用 JSX 語法時,不再需要手動引入React庫。在自動運行時模式下,JSX 會被轉換成新的入口函數,import {jsx as _jsx} from 'react/jsx-runtime'; 和 import {jsxs as _jsxs} from 'react/jsx-runtime';

轉換結果如下:

import { jsx as _jsx } from "react/jsx-runtime";

const element = _jsx("div"{ 
  className: "container", 
  children: "Hello, world!" 
});

接下來我們就來實現jsx方法或React.createElement方法(包括 dev、prod 兩個環境)。

工作量包括:

實現 jsx 轉換方法

jsx 轉換方法包括:

實現 React.createElement

在 React 17 之前,JSX 轉換應用的是createElement方法,下面是它的實現:

/**
 * 
 * @param type 元素類型
 * @param config 元素屬性,包括key,不包括子元素children
 * @param maybeChildren 子元素children
 * @returns 返回一個ReactElement
 */
const createElement = (
  type: ElementType, 
  config: any, 
  ...maybeChildren: any
) ={
  // reactElement 自身的屬性
  let key: Key = null;
  let ref: Ref = null;

  // 創建一個空對象props,用於存儲屬性
  const props: Props = {};

  // 遍歷config對象,將ref、key這些ReactElement內部使用的屬性提取出來,不應該被傳遞下去
  for (const prop in config) {
    const val = config[prop];
    if (prop === 'key') {
      if (val !== undefined) {
        key = '' + val;
      }
      continue;
    }
    if (prop === 'ref') {
      if (val !== undefined) {
        ref = val;
      }
      continue;
    }
    // 去除config原型鏈上的屬性,只要自身
    // 一般使用{...props}將所有屬性都傳遞下去,所以摘除ref、key屬性外需要被保存到props中
    if ({}.hasOwnProperty.call(config, prop)) {
      props[prop] = val;
    }
  }

  const maybeChildrenLength = maybeChildren.length;
  if (maybeChildrenLength) {
    // [child] [child, child, child]
    if (maybeChildrenLength === 1) {
      props.children = maybeChildren[0];
    } else {
      props.children = maybeChildren;
    }
  }

  return ReactElement(type, key, ref, props);
};

注意:React.createElement 方法和 jsx 方法的區別這裏只體現在第三個參數上

實現 jsx 方法

從 React 17 之後,JSX 轉換應用的是jsx方法,下面是它的實現:

/**
 * 
 * @param type 元素類型
 * @param config 元素屬性
 * @param maybeKey 可能的key值
 * @returns 返回一個ReactElement
 */
const jsx = (type: ElementType, config: any, maybeKey: any) ={
  // 初始化key和ref爲空
  let key = null;
  let ref = null;

  // 創建一個空對象props,用於存儲屬性
  const props: Props = {};

  // 遍歷config對象,將ref、key這些ReactElement內部使用的屬性提取出來,不應該被傳遞下去
  for (const prop in config) {
    const val = config[prop];
    if (prop === "key") {
      continue;
    }
    if (prop === "ref") {
      if (val !== undefined) {
        ref = val;
      }
      continue;
    }
    // 一般使用{...props}將所有屬性都傳遞下去,所以摘除ref、key屬性外需要被保存到props中
    if ({}.hasOwnProperty.call(config, prop)) {
      props[prop] = val;
    }
  }

  // 將 maybeKey 添加到 key 中
  if (maybeKey !== undefined) {
    key = "" + maybeKey;
  }

  return ReactElement(type, key, ref, props);
};

這段代碼定義了一個jsx函數,主要用於創建 React 元素。首先,它會提取可能存在的 key 和 ref 屬性,並將剩餘屬性添加到一個新的 props 對象中。最後用ReactElement函數創建一個 React 元素並返回。

從上面代碼中可以看到還實現了ReactElement方法

// jsx-runtime.js
const supportSymbol = typeof Symbol === 'function' && Symbol.for;

// 爲了不濫用 React.elemen,所以爲它創建一個單獨的鍵
// 爲React.element元素創建一個 symbol 並放入到 symbol 註冊表中
export const REACT_ELEMENT_TYPE = supportSymbol
  ? Symbol.for('react.element')
  : 0xeac7;

export const ReactElement = function (type, key, ref, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type,
    key,
    ref,
    props,
    _mark: 'lsh',
  };
  return element;
};

export const jsx =...

用自己實現的的 jsx 接入 Demo

我們試着把自己實現的jsx方法,創建一個ReactElement,看它是否能夠渲染在頁面上。

jsx-Demo 運行地址

jsx 方法和 createElement 的區別

jsx函數和createElement函數都用於在 React 中創建虛擬 DOM 元素,但它們的語法和用法有所不同。jsx 函數來自於 React 17 及更高版本中的新的 JSX 轉換功能,稱爲 "Runtime Automatic"。

以下是兩者之間的主要區別:

  1. 語法和轉換方式jsx函數用於處理新的 JSX 轉換方式,其語法更簡潔。createElement函數用於處理傳統的 JSX 轉換方式。

例如,一個 JSX 元素:

const element = <div class>Hello, world!</div>;

使用createElement轉換後的代碼如下:

const element = React.createElement(
  "div",
  { className: "container" },
  "Hello, world!"
);

使用jsx函數(自動運行時)轉換後的代碼如下:

import { jsx as _jsx } from "react/jsx-runtime";

const element = _jsx("div"{ className: "container", children: "Hello, world!" });
  1. 子元素和 key 值處理jsx函數將子元素作爲屬性(children)傳遞,而createElement函數將子元素作爲額外的參數傳遞。同時子元素上的key值在jsx函數中也會以第三個參數的形式傳遞,而在createElement函數中,則是存在於config第二個參數中。

createElement函數中:

React.createElement("div"{className: "app", key: "appKey"}"hello,app");

jsx函數中:

import { jsx as _jsx } from "react/jsx-runtime";

_jsx("div"{className: "app", children: "hello,app"}"appKey");
  1. 兼容性和版本createElement函數在所有 React 版本中可用,而 jsx 函數僅在 React 17 及更高版本中提供。儘管 React 團隊推薦使用新的 JSX 轉換方式,但許多現有項目可能仍在使用createElement函數。

這時可能產生兩個疑問:

  1. 簡化組件代碼:不再需要在每個組件文件頂部添加 import React from 'react';。這使得組件代碼更簡潔,更易於閱讀和維護。

  2. 節省包大小:由於不再需要導入整個 React 對象,構建工具可以更好地優化輸出代碼,從而減小輸出包的大小。

在之前的 React 版本中,每當創建一個新的 React 元素時,React 都需要從屬性對象中提取keyref,這會導致額外的性能開銷。

key作爲單獨的參數傳遞,可以讓 React 在處理虛擬 DOM 樹時更容易地訪問key,無需每次都從屬性對象中查找。這有助於提高 React 的性能和效率,特別是在處理大量元素和複雜組件樹時。

實現打包流程

打包流程稍微有些複雜,後續寫到文章裏。

簡單來說就是使用 Rollup,將編寫 jsx 方法的文件打包出來,通過pnpm link --global的方式生成一個全局的react包,這樣就可以通過pnpm link react --global調試自己創建的 create-react-app demo 項目了。

big-react 項目地址

github big-react

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/98p5O0wpe1AL6jSUEkbu_A