「深入淺出」實現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 是什么
在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元素。
轉換結果如下:
從React 17開始,引入了新的JSX轉換功能,稱為"Runtime Automatic"(自動運行時)。這意味著在使用JSX語法時,不再需要手動引入React庫。在自動運行時模式下,JSX會被轉換成新的入口函數,import {jsx as _jsx} from 'react/jsx-runtime'; 和 import {jsxs as _jsxs} from 'react/jsx-runtime';。
轉換結果如下:
接下來我們就來實現jsx方法或React.createElement方法(包括dev、prod兩個環境)。
工作量包括:
- 實現jsx方法
- 實現打包流程
- 實現調試打包結果的環境
實現 jsx 轉換方法
jsx 轉換方法包括:
- React.createElement方法
- jsxDEV方法(dev環境)
- jsx方法(prod環境)
實現React.createElement
在React 17之前,JSX轉換應用的是createElement方法,下面是它的實現:
注意:React.createElement方法和jsx方法的區別這里只體現在第三個參數上。
實現jsx方法
從React 17之后,JSX轉換應用的是jsx方法,下面是它的實現:
這段代碼定義了一個jsx函數,主要用于創建React元素。首先,它會提取可能存在的key和ref屬性,并將剩余屬性添加到一個新的props對象中。最后用ReactElement函數創建一個React元素并返回。
從上面代碼中可以看到還實現了ReactElement方法:
用自己實現的的jsx接入Demo
我們試著把自己實現的jsx方法,創建一個ReactElement,看它是否能夠渲染在頁面上。
實現jsx方法
jsx-Demo運行地址
jsx方法和createElement的區別
jsx函數和createElement函數都用于在React中創建虛擬DOM元素,但它們的語法和用法有所不同。jsx函數來自于React 17及更高版本中的新的JSX轉換功能,稱為"Runtime Automatic"。
以下是兩者之間的主要區別:
- 語法和轉換方式:jsx函數用于處理新的JSX轉換方式,其語法更簡潔。createElement函數用于處理傳統的JSX轉換方式。
例如,一個JSX元素:
使用createElement轉換后的代碼如下:
使用jsx函數(自動運行時)轉換后的代碼如下:
- ?子元素和key值處理:jsx函數將子元素作為屬性(children)傳遞,而createElement函數將子元素作為額外的參數傳遞。同時子元素上的key值在jsx函數中也會以第三個參數的形式傳遞,而在createElement函數中,則是存在于config第二個參數中。
在createElement函數中:
在jsx函數中:
- ?兼容性和版本: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項目了。
構建react包思路