「深入淺出」實現(xiàn)JSX的轉(zhuǎn)換
前言
由于近期在看React框架源碼、底層實現(xiàn)方面的知識,所以想把學(xué)習(xí)心得整理出來。
這也是一個新的系列「從0實現(xiàn)React 18核心模塊」的第一篇。
接下來還會更新:render、commit階段的實現(xiàn),以及Hooks架構(gòu)、useState、useEffect、單雙節(jié)點Diff的過程還有React 18中的并發(fā)更新原理。
在看文章之前,我們可以先想幾個問題:
- JSX 是什么語法?
- JSX 有什么優(yōu)勢,它的轉(zhuǎn)換規(guī)則是什么或者它內(nèi)部是如何實現(xiàn)的?
- 既然 React 一直在使用 JSX,那它的實現(xiàn)被寫應(yīng)該寫在哪個包里(比如react、react-dom,react-reconciler)?
- 在React 17之前和React 17之后,JSX轉(zhuǎn)換的方法實現(xiàn)有哪些異同?
- 如何實現(xiàn)React.createElement方法和運行時的 jsx 方法?
- 寫一個Demo引入自己實現(xiàn)的jsx方法,看看運行結(jié)果
下文提到的 big-react 是從0到1實現(xiàn)的React的核心功能模塊原理的項目
如果自己實現(xiàn)一個 React 框架,它需要包含哪些內(nèi)置的包:
- react包是 React 的核心庫,提供了創(chuàng)建和管理組件所需的基本功能(比如組件創(chuàng)建、組件生命周期管理、虛擬DOM以及Hooks等),主要是一些和宿主環(huán)境無關(guān)的方法。
- react-reconciler包實現(xiàn)了 React 的 reconciliation 協(xié)調(diào)算法,是一種核心優(yōu)化策略的實現(xiàn),主要自定義協(xié)調(diào)器的實現(xiàn)。以及在不同的平臺或環(huán)境中使用 React。
- shared包是big-react公用的輔助方法,和宿主環(huán)境無關(guān)。
如果還有一個必要的包,那就是react-dom:
- react-dom:這個包提供了將 React 與 DOM(瀏覽器環(huán)境)集成的方法。它包含了用于將 React 組件渲染到 DOM 中的 ReactDOM.render() 函數(shù),以及其他與瀏覽器環(huán)境相關(guān)的實用功能。對于在瀏覽器中運行的 React 應(yīng)用程序,react-dom 是必需的。
react與react-reconciler包是什么
react包為我們提供了什么
當(dāng)我們在項目中使用 React 構(gòu)建界面時,主要使用的就是 react? 包。它提供了開發(fā)者需要的所有API。如React.Component、React.createElement、React.useState等等,所以它也是大多數(shù) React 項目的基礎(chǔ)。
react-reconciler包實現(xiàn)了什么?
react-reconciler包是一個更底層、更高級的庫,它實現(xiàn)了reconciliation協(xié)調(diào)算法,reconciliation是 React 的一種核心優(yōu)化策略,用于在更新組件時比較虛擬DOM樹的差異,并將實際更改應(yīng)用到實際的DOM樹。這有助于提高性能,因為避免了不必要的DOM操作。
它主要用于創(chuàng)建自定義渲染器,以及在不同的平臺中去使用 React。例如,react-dom(用于Web平臺)和react-native(用于移動應(yīng)用)都使用react-reconciler作為底層庫,實現(xiàn)了針對各自平臺的渲染邏輯。
JSX 是什么
在React中,JSX是一種JavaScript語法擴展,允許你在JavaScript代碼中編寫類似HTML的標(biāo)記。要使用JSX,需要在構(gòu)建過程中將其轉(zhuǎn)換為標(biāo)準(zhǔn)的JavaScript代碼。
通常,這個轉(zhuǎn)換過程包括兩個主要部分:
- 編譯時:通常指將 JSX 語法轉(zhuǎn)換為瀏覽器可以理解的普通 JavaScript 代碼的過程,這個過程通常由 Babel 完成。
- 構(gòu)建時:在將JSX語法轉(zhuǎn)換為標(biāo)準(zhǔn)的JavaScript代碼后,通常會使用構(gòu)建和打包工具(如Webpack、Rollup)對代碼進行優(yōu)化、壓縮和打包。打包工具將源代碼和依賴項組合成一個或多個文件(“bundles”或“chunks”),用于在瀏覽器中運行。
- 運行時:React會根據(jù)編譯后的代碼創(chuàng)建虛擬DOM樹,然后將其渲染到實際的DOM中。還會發(fā)生的階段有狀態(tài)管理和更新、事件處理和Diff算法的比較等。
JSX 被 Babel 編譯成了什么
在React 17之前,JSX語法會被編譯成React.createElement函數(shù)的調(diào)用,用來創(chuàng)建虛擬DOM元素。
轉(zhuǎn)換結(jié)果如下:
從React 17開始,引入了新的JSX轉(zhuǎn)換功能,稱為"Runtime Automatic"(自動運行時)。這意味著在使用JSX語法時,不再需要手動引入React庫。在自動運行時模式下,JSX會被轉(zhuǎn)換成新的入口函數(shù),import {jsx as _jsx} from 'react/jsx-runtime'; 和 import {jsxs as _jsxs} from 'react/jsx-runtime';。
轉(zhuǎn)換結(jié)果如下:
接下來我們就來實現(xiàn)jsx方法或React.createElement方法(包括dev、prod兩個環(huán)境)。
工作量包括:
- 實現(xiàn)jsx方法
- 實現(xiàn)打包流程
- 實現(xiàn)調(diào)試打包結(jié)果的環(huán)境
實現(xiàn) jsx 轉(zhuǎn)換方法
jsx 轉(zhuǎn)換方法包括:
- React.createElement方法
- jsxDEV方法(dev環(huán)境)
- jsx方法(prod環(huán)境)
實現(xiàn)React.createElement
在React 17之前,JSX轉(zhuǎn)換應(yīng)用的是createElement方法,下面是它的實現(xiàn):
注意:React.createElement方法和jsx方法的區(qū)別這里只體現(xiàn)在第三個參數(shù)上。
實現(xiàn)jsx方法
從React 17之后,JSX轉(zhuǎn)換應(yīng)用的是jsx方法,下面是它的實現(xiàn):
這段代碼定義了一個jsx函數(shù),主要用于創(chuàng)建React元素。首先,它會提取可能存在的key和ref屬性,并將剩余屬性添加到一個新的props對象中。最后用ReactElement函數(shù)創(chuàng)建一個React元素并返回。
從上面代碼中可以看到還實現(xiàn)了ReactElement方法:
用自己實現(xiàn)的的jsx接入Demo
我們試著把自己實現(xiàn)的jsx方法,創(chuàng)建一個ReactElement,看它是否能夠渲染在頁面上。
實現(xiàn)jsx方法
jsx-Demo運行地址
jsx方法和createElement的區(qū)別
jsx函數(shù)和createElement函數(shù)都用于在React中創(chuàng)建虛擬DOM元素,但它們的語法和用法有所不同。jsx函數(shù)來自于React 17及更高版本中的新的JSX轉(zhuǎn)換功能,稱為"Runtime Automatic"。
以下是兩者之間的主要區(qū)別:
- 語法和轉(zhuǎn)換方式:jsx函數(shù)用于處理新的JSX轉(zhuǎn)換方式,其語法更簡潔。createElement函數(shù)用于處理傳統(tǒng)的JSX轉(zhuǎn)換方式。
例如,一個JSX元素:
使用createElement轉(zhuǎn)換后的代碼如下:
使用jsx函數(shù)(自動運行時)轉(zhuǎn)換后的代碼如下:
- ?子元素和key值處理:jsx函數(shù)將子元素作為屬性(children)傳遞,而createElement函數(shù)將子元素作為額外的參數(shù)傳遞。同時子元素上的key值在jsx函數(shù)中也會以第三個參數(shù)的形式傳遞,而在createElement函數(shù)中,則是存在于config第二個參數(shù)中。
在createElement函數(shù)中:
在jsx函數(shù)中:
- ?兼容性和版本:createElement函數(shù)在所有React版本中可用,而jsx函數(shù)僅在React 17及更高版本中提供。盡管React團隊推薦使用新的JSX轉(zhuǎn)換方式,但許多現(xiàn)有項目可能仍在使用createElement函數(shù)。
這時可能產(chǎn)生兩個疑問:
- 從React 17之后使用Runtime Automatic自動運行時有什么好處?
- 簡化組件代碼:不再需要在每個組件文件頂部添加**import React from 'react';**。這使得組件代碼更簡潔,更易于閱讀和維護。
- 節(jié)省包大?。河捎诓辉傩枰獙?dǎo)入整個React對象,構(gòu)建工具可以更好地優(yōu)化輸出代碼,從而減小輸出包的大小。
- 改成jsx函數(shù)后,為什么要把key屬性單獨拿出來放在第三個參數(shù)?
在之前的React版本中,每當(dāng)創(chuàng)建一個新的React元素時,React都需要從屬性對象中提取key?和ref,這會導(dǎo)致額外的性能開銷。
將key?作為單獨的參數(shù)傳遞,可以讓React在處理虛擬DOM樹時更容易地訪問key,無需每次都從屬性對象中查找。這有助于提高React的性能和效率,特別是在處理大量元素和復(fù)雜組件樹時。
實現(xiàn)打包流程
打包流程稍微有些復(fù)雜,后續(xù)寫到文章里。
簡單來說就是使用 Rollup,將編寫jsx方法的文件打包出來,通過pnpm link --global的方式生成一個全局的react包,這樣就可以通過pnpm link react --global調(diào)試自己創(chuàng)建的 create-react-app demo項目了。
構(gòu)建react包思路