「深入淺出」實現 JSX 的轉換
前言
由於近期在看React
框架源碼、底層實現方面的知識,所以想把學習心得整理出來。
這也是一個新的系列**「從 0 實現 React 18 核心模塊」**的第一篇。
接下來還會更新:render、commit 階段的實現,以及 Hooks 架構、useState、useEffect、單雙節點 Diff 的過程還有 React 18 中的併發更新原理。
在看文章之前,我們可以先想幾個問題:
-
JSX 是什麼語法?
-
JSX 有什麼優勢,它的轉換規則是什麼或者它內部是如何實現的?
-
既然 React 一直在使用 JSX,那它的實現被寫應該寫在哪個包裏(比如
react、react-dom,react-reconciler
)? -
在 React 17 之前和 React 17 之後,JSX 轉換的方法實現有哪些異同?
-
如何實現 React.createElement 方法和運行時的 jsx 方法?
-
寫一個 Demo 引入自己實現的 jsx 方法,看看運行結果
下文提到的 big-react
是從 0 到 1 實現的 React 的核心功能模塊原理的項目
如果自己實現一個 React 框架,它需要包含哪些內置的包:
-
react
包是 React 的核心庫,提供了創建和管理組件所需的基本功能(比如組件創建、組件生命週期管理、虛擬 DOM 以及 Hooks 等),主要是一些和宿主環境無關的方法。 -
react-reconciler
包實現了 React 的reconciliation
協調算法,是一種核心優化策略的實現,主要自定義協調器的實現。以及在不同的平臺或環境中使用 React。 -
shared
包是 big-react 公用的輔助方法,和宿主環境無關。
如果還有一個必要的包,那就是react-dom
:
react-dom
:這個包提供了將 React 與 DOM(瀏覽器環境)集成的方法。它包含了用於將 React 組件渲染到 DOM 中的ReactDOM.render()
函數,以及其他與瀏覽器環境相關的實用功能。對於在瀏覽器中運行的 React 應用程序,react-dom
是必需的。
react 與 react-reconciler 包是什麼
react 包爲我們提供了什麼
當我們在項目中使用 React 構建界面時,主要使用的就是 react
包。它提供了開發者需要的所有 API。如React.Component
、React.createElement
、React.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 語法轉換爲瀏覽器可以理解的普通 JavaScript 代碼的過程,這個過程通常由 Babel 完成。
-
構建時:在將 JSX 語法轉換爲標準的 JavaScript 代碼後,通常會使用構建和打包工具(如 Webpack、Rollup)對代碼進行優化、壓縮和打包。打包工具將源代碼和依賴項組合成一個或多個文件(“bundles” 或 “chunks”),用於在瀏覽器中運行。
-
運行時:React 會根據編譯後的代碼創建虛擬 DOM 樹,然後將其渲染到實際的 DOM 中。還會發生的階段有狀態管理和更新、事件處理和 Diff 算法的比較等。
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 轉換方法
jsx 轉換方法包括:
-
React.createElement
方法 -
jsxDEV方法
(dev 環境) -
jsx
方法(prod 環境)
實現 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"。
以下是兩者之間的主要區別:
- 語法和轉換方式:
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!" });
- 子元素和 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");
- 兼容性和版本:
createElement
函數在所有 React 版本中可用,而 jsx 函數僅在 React 17 及更高版本中提供。儘管 React 團隊推薦使用新的 JSX 轉換方式,但許多現有項目可能仍在使用createElement
函數。
這時可能產生兩個疑問:
- 從 React 17 之後使用
Runtime Automatic
自動運行時有什麼好處?
-
簡化組件代碼:不再需要在每個組件文件頂部添加 import React from 'react';。這使得組件代碼更簡潔,更易於閱讀和維護。
-
節省包大小:由於不再需要導入整個 React 對象,構建工具可以更好地優化輸出代碼,從而減小輸出包的大小。
- 改成
jsx
函數後,爲什麼要把key
屬性單獨拿出來放在第三個參數?
在之前的 React 版本中,每當創建一個新的 React 元素時,React 都需要從屬性對象中提取key
和ref
,這會導致額外的性能開銷。
將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