React jsx轉(zhuǎn)換與createElement使用超詳細講解
jsx的轉(zhuǎn)換
我們從 react 應(yīng)用的入口開始對源碼進行分析,創(chuàng)建一個簡單的 hello, world 應(yīng)用:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
export default class App extends Component {
render() {
return <div>hello, world</div>;
}
}
ReactDOM.render(<App />, document.getElementById('root'));我們注意到,我們在 App 組件中直接寫了 return <div>hello, world</div> 的 jsx 語句,那么 jsx 語法是如何被瀏覽器識別執(zhí)行的呢?
另外我在第一次學(xué)習(xí) react 的時候,就有一個疑惑: import React, { Component } from 'react' 這段代碼中,React 似乎在代碼中沒有任何地方被用到,為什么要引入呢?
16.x版本及之前
我們在 react16.8 版本的代碼中,嘗試將 React 的引用去掉:
// import React, { Component } from 'react';
import { Component } from 'react'; // 去掉 React 的引用
import ReactDOM from 'react-dom';
export default class App extends Component {
render() {
return <div>hello, world</div>;
}
}
ReactDOM.render(<App />, document.getElementById('root'));運行應(yīng)用程序,發(fā)現(xiàn)會提示 'React' must be in scope when using JSX 的 error:
這是因為上述的類組件 render 中返回了 <div>hello, world</div> 的 jsx 語法,在React16版本及之前,應(yīng)用程序通過 @babel/preset-react 將 jsx 語法轉(zhuǎn)換為 React.createElement 的 js 代碼,因此需要顯式將 React 引入,才能正常調(diào)用 createElement。我們可以在 Babel REPL 中看到 jsx 被 @babel/preset-react 編譯后的結(jié)果
17.x版本及之后
React17版本之后,官方與 bbel 進行了合作,直接通過將 react/jsx-runtime 對 jsx 語法進行了新的轉(zhuǎn)換而不依賴 React.createElement,轉(zhuǎn)換的結(jié)果便是可直接供 ReactDOM.render 使用的 ReactElement 對象。因此如果在React17版本后只是用 jsx 語法不使用其他的 react 提供的api,可以不引入 React,應(yīng)用程序依然能夠正常運行。
更多有關(guān)于 React jsx 轉(zhuǎn)換的內(nèi)容可以去看官網(wǎng)了解:介紹全新的JSX轉(zhuǎn)換,在這里就不再過多展開了。
React.createElement源碼
雖然現(xiàn)在 react17 之后我們可以不再依賴 React.createElement 這個 api 了,但是實際場景中以及很多開源包中可能會有很多通過 React.createElement 手動創(chuàng)建元素的場景,所以還是推薦學(xué)習(xí)一下React.createElement源碼。
React.createElement 其接收三個或以上參數(shù):
- type:要創(chuàng)建的 React 元素類型,可以是標簽名稱字符串,如
'div'或者'span'等;也可以是 React組件 類型(class組件或者函數(shù)組件);或者是 React fragment 類型。 - config:寫在標簽上的屬性的集合,js 對象格式,若標簽上未添加任何屬性則為 null。
- children:從第三個參數(shù)開始后的參數(shù)為當前創(chuàng)建的React元素的子節(jié)點,每個參數(shù)的類型,若是當前元素節(jié)點的 textContent 則為字符串類型;否則為新的 React.createElement 創(chuàng)建的元素。
函數(shù)中會對參數(shù)進行一系列的解析,源碼如下,對源碼相關(guān)的理解都用注釋進行了標記:
export function createElement(type, config, children) {
let propName;
// 記錄標簽上的屬性集合
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
// config 不為 null 時,說明標簽上有屬性,將屬性添加到 props 中
// 其中,key 和 ref 為 react 提供的特殊屬性,不加入到 props 中,而是用 key 和 ref 單獨記錄
if (config != null) {
if (hasValidRef(config)) {
// 有合法的 ref 時,則給 ref 賦值
ref = config.ref;
if (__DEV__) {
warnIfStringRefCannotBeAutoConverted(config);
}
}
if (hasValidKey(config)) {
// 有合法的 key 時,則給 key 賦值
key = '' + config.key;
}
// self 和 source 是開發(fā)環(huán)境下對代碼在編譯器中位置等信息進行記錄,用于開發(fā)環(huán)境下調(diào)試
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 將 config 中除 key、ref、__self、__source 之外的屬性添加到 props 中
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// 將子節(jié)點添加到 props 的 children 屬性上
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
// 共 3 個參數(shù)時表示只有一個子節(jié)點,直接將子節(jié)點賦值給 props 的 children 屬性
props.children = children;
} else if (childrenLength > 1) {
// 3 個以上參數(shù)時表示有多個子節(jié)點,將子節(jié)點 push 到一個數(shù)組中然后將數(shù)組賦值給 props 的 children
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 開發(fā)環(huán)境下凍結(jié) childArray,防止被隨意修改
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// 如果有 defaultProps,對其遍歷并且將用戶在標簽上未對其手動設(shè)置屬性添加進 props 中
// 此處針對 class 組件類型
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// key 和 ref 不掛載到 props 上
// 開發(fā)環(huán)境下若想通過 props.key 或者 props.ref 獲取則 warning
if (__DEV__) {
if (key || ref) {
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
// 調(diào)用 ReactElement 并返回
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}相關(guān)參考視頻:傳送門
由代碼可知,React.createElement 做的事情主要有:
- 解析 config 參數(shù)中是否有合法的 key、ref、__source 和 __self 屬性,若存在分別賦值給 key、ref、source 和 self;將剩余的屬性解析掛載到 props 上
- 除 type 和 config 外后面的參數(shù),掛載到
props.children上 - 針對類組件,如果 type.defaultProps 存在,遍歷 type.defaultProps 的屬性,如果 props 不存在該屬性,則添加到 props 上
- 將 type、key、ref、self、props 等信息,調(diào)用
ReactElement函數(shù)創(chuàng)建虛擬 dom,ReactElement主要是在開發(fā)環(huán)境下通過Object.defineProperty將 _store、_self、_source 設(shè)置為不可枚舉,提高 element 比較時的性能:
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 用于表示是否為 ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 用于創(chuàng)建真實 dom 的相關(guān)信息
type: type,
key: key,
ref: ref,
props: props,
_owner: owner,
};
if (__DEV__) {
element._store = {};
// 開發(fā)環(huán)境下將 _store、_self、_source 設(shè)置為不可枚舉,提高 element 的比較性能
Object.defineProperty(element._store, 'validated', {
configurable: false,
enumerable: false,
writable: true,
value: false,
});
Object.defineProperty(element, '_self', {
configurable: false,
enumerable: false,
writable: false,
value: self,
});
Object.defineProperty(element, '_source', {
configurable: false,
enumerable: false,
writable: false,
value: source,
});
// 凍結(jié) element 和 props,防止被手動修改
if (Object.freeze) {
Object.freeze(element.props);
Object.freeze(element);
}
}
return element;
};所以通過流程圖總結(jié)一下 createElement 所做的事情如下:

React.Component 源碼
我們回到上述 hello,world 應(yīng)用程序代碼中,創(chuàng)建類組件時,我們繼承了從 react 庫中引入的 Component,我們再看一下React.Component源碼:
function Component(props, context, updater) {
// 接收 props,context,updater 進行初始化,掛載到 this 上
this.props = props;
this.context = context;
this.refs = emptyObject;
// updater 上掛載了 isMounted、enqueueForceUpdate、enqueueSetState 等觸發(fā)器方法
this.updater = updater || ReactNoopUpdateQueue;
}
// 原型鏈上掛載 isReactComponent,在 ReactDOM.render 時用于和函數(shù)組件做區(qū)分
Component.prototype.isReactComponent = {};
// 給類組件添加 `this.setState` 方法
Component.prototype.setState = function(partialState, callback) {
// 驗證參數(shù)是否合法
invariant(
typeof partialState === 'object' ||
typeof partialState === 'function' ||
partialState == null
);
// 添加至 enqueueSetState 隊列
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
// 給類組件添加 `this.forceUpdate` 方法
Component.prototype.forceUpdate = function(callback) {
// 添加至 enqueueForceUpdate 隊列
this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};從源碼上可以得知,React.Component 主要做了以下幾件事情:
- 將 props, context, updater 掛載到 this 上
- 在 Component 原型鏈上添加 isReactComponent 對象,用于標記類組件
- 在 Component 原型鏈上添加
setState方法 - 在 Component 原型鏈上添加
forceUpdate方法,這樣我們就理解了 react 類組件的super()作用,以及this.setState和this.forceUpdate的由來
總結(jié)
本章講述了 jsx 在 react17 之前和之后的不同的轉(zhuǎn)換,實際上 react17 之后 babel 的對 jsx 的轉(zhuǎn)換就是比之前多了一步 React.createElement 的動作:

另外講述了 React.createElement 和 React.Component 的內(nèi)部實現(xiàn)是怎樣的。通過 babel及 React.createElement,將 jsx 轉(zhuǎn)換為了瀏覽器能夠識別的原生 js 語法,為 react 后續(xù)對狀態(tài)改變、事件響應(yīng)以及頁面更新等奠定了基礎(chǔ)。
后面的章節(jié)中,將探究 react 是如何一步步將狀態(tài)等信息渲染為真實頁面的。
到此這篇關(guān)于React jsx轉(zhuǎn)換與createElement使用超詳細講解的文章就介紹到這了,更多相關(guān)React jsx轉(zhuǎn)換與createElement內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React類組件中super()和super(props)的區(qū)別詳解
這篇文章給大家詳細介紹了React類組件中super()和super(props)有什么區(qū)別,文中通過代碼示例給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-01-01
React實現(xiàn)類似淘寶tab居中切換效果的示例代碼
這篇文章主要介紹了React實現(xiàn)類似淘寶tab居中切換效果,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06

