JSX 的本質#
JSX 到底是什麼,我們先來看看 React 官網給出的一段定義:
JSX 是 JavaScript 的一種語法擴展,它和模板語言很接近,但是它充分具備 JavaScript 的能力。
對於 JSX,facebook 定義為 “語法擴展”,同時充分具備JS 的能力。但事實上人家長得和 HTML 很像,並不像熟知的 JavaScript,這怎麼解釋呢?
JSX 語法和 JavaScript 關係#
實際上答案很簡單,見React 官網
JSX 會被編譯為 React.createElement (), React.createElement () 將返回一個叫作 “React Element” 的 JS 對象。
所以說其實 JSX 是會通過一次編譯後,通過React.createElement()
的調用變成一個React Element
的 js 對象。
那麼編譯是如何做到的呢?其實對於ECMAScript 2015+ 版本
的代碼,我們通常需要使用一個工具Babel
來對舊版本的瀏覽器做兼容。
比如ES2015+
中很好用的模板字符串語法糖:
var text = 'World'
console.log(`Hello ${text}!`) //Hello World!
Babel 就可以幫我們把這段代碼轉換為大部分低版本瀏覽器也能夠識別的 ES5 代碼:
var text = 'World'
console.log('Hello'.concat(text, '!')) //Hello World!
類似的,Babel 也具備將 JSX 語法轉換為 JavaScript 代碼的能力。 那麼 Babel 具體會將 JSX 處理成什麼樣子呢?【例子】
可以看到,JSX 的標籤都被轉化成了對應的 React.createElement 調用,所以說實際上 JSX 就是寫的 React.createElement,所以說它看起來像 HTML,但內核是 JS 罷了。
JSX 的本質是 React.createElement 這個 JavaScript 調用的語法糖,這也就完美地呼應上了 React 官方給出的 “JSX 充分具備 JavaScript 的能力” 這句話。
如上圖,JSX 的優勢很明顯,JSX 代碼層次分明,嵌套關係清晰;但是使用 React.createElement 看著就比 JSX 混亂的多,讀起來不友好,寫起來也費勁。
JSX 語法糖允許前端開發者使用我們最為熟悉的類 HTML 標籤語法來創建虛擬 DOM,在降低學習成本的同時,也提升了研發效率與研發體驗。
JSX 是如何映射為 DOM 的?#
先來看看 createElement 源碼,這裡是一段抄來的有註釋的源碼
export function createElement(type, config, children) {
var propName
//提取保留名稱
var props = {}
var key = null
var ref = null
var self = null
var source = null
//標籤的屬性不為空時,說明標籤有屬性值,特殊處理:把key和ref賦值給單獨的變量
if (config != null) {
//有合理的ref
if (hasValidRef(config)) {
ref = config.ref
}
//有合理的key
if (hasValidKey(config)) {
key = '' + config.key
}
self = config.__self === undefined ? null : config.__self
source = config.__source === undefined ? null : config.__source
//config中剩餘屬性,且不是原生屬性(RESERVED_PROPS對象的屬性),則添加到新props對象中
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName] //config去除key/ref 其他屬性的放到props對象中
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
// 子元素數量(第三個參數以及之後參數都是子元素 兄弟節點)
var childrenLength = arguments.length - 2
if (childrenLength === 1) {
props.children = children
} else if (childrenLength > 1) {
var childArray = Array(childrenLength) //聲明一個數組
//依次將children push到數組中
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2]
}
{
//凍結array 返回原來的childArray且不能被修改 防止有人修改庫的核心對象 凍結對象大大提高性能
if (Object.freeze) {
Object.freeze(childArray)
}
}
props.children = childArray //父組件內部通過this.props.children獲取子組件的值
}
//為子組件設置默認值 一般針對的是組件
//class com extends React.component 則com.defaultProps獲取當前組件自己的靜態方法
if (type && type.defaultProps) {
//如果當前組件中有默認的defaultProps則把當前組件的默認內容 定義到defaultProps中
var defaultProps = type.defaultProps
for (propName in defaultProps) {
if (props[propName] === undefined) {
//如果父組件中對應的值為undefined 則把默認值賦值賦值給props當作props的屬性
props[propName] = defaultProps[propName]
}
}
}
{
//一旦ref或者key存在
if (key || ref) {
//如果type是組件的話
var displayName =
typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type
if (key) {
defineKeyPropWarningGetter(props, displayName)
}
if (ref) {
defineRefPropWarningGetter(props, displayName)
}
}
}
//props:1.config的屬性值 2.children的屬性(字符串/數組)3.default的屬性值
return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props)
}
參數分析#
function createElement(type, config, children)
擁有三個參數type, config, children
對應的含義
type:用於標識節點的類型。它可以是類似 “h1”“div” 這樣的標準 HTML 標籤字符串,也可以是 React 組件類型或 React fragment 類型。
config:以對象形式傳入,組件所有的屬性都會以鍵值對的形式存儲在 config 對象中。
children:以對象形式傳入,它記錄的是組件標籤之間嵌套的內容,也就是所謂的 “子節點”“子元素”。
比如下面的調用示例
React.createElement(
'ul',
{
// 傳入屬性鍵值對
className: 'list',
// 從第三個入參開始往後,傳入的參數都是 children
},
React.createElement(
'li',
{
key: '1',
},
'1'
),
React.createElement(
'li',
{
key: '2',
},
'2'
)
)
它對應的 DOM 結構如下
<ul className="list">
<li key="1">1</li>
<li key="2">2</li>
</ul>
了解參數過後繼續往下走
拆解 config 參數#
if (config != null) {
//有合理的ref
if (hasValidRef(config)) {
ref = config.ref
}
//有合理的key
if (hasValidKey(config)) {
key = '' + config.key
}
self = config.__self === undefined ? null : config.__self
source = config.__source === undefined ? null : config.__source
//config中剩餘屬性,且不是原生屬性(RESERVED_PROPS對象的屬性),則添加到新props對象中
for (propName in config) {
if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
props[propName] = config[propName] //config去除key/ref 其他屬性的放到props對象中
}
}
}
此處將入參的 config 拆解成ref, key, self, source, props
幾個屬性,緊接著就是處理 children 的部分
提取子元素#
從上面分解 config 之後是處理子元素的代碼,此處將第二個參數以後的所有參數都存入props.children
數組
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
// 子元素數量(第三個參數以及之後參數都是子元素 兄弟節點)
var childrenLength = arguments.length - 2
if (childrenLength === 1) {
props.children = children
} else if (childrenLength > 1) {
var childArray = Array(childrenLength) //聲明一個數組
//依次將children push到數組中
for (var i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2]
}
{
//凍結array 返回原來的childArray且不能被修改 防止有人修改庫的核心對象 凍結對象大大提高性能
if (Object.freeze) {
Object.freeze(childArray)
}
}
props.children = childArray //父組件內部通過this.props.children獲取子組件的值
}
接下來就是處理當父組件給 children 傳入 props 情況,如果子組件設置了默認值並且父組件未傳入 props (即值為undefined
) 時使用提供的默認值。
//為子組件設置默認值 一般針對的是組件
//class com extends React.component 則com.defaultProps獲取當前組件自己的靜態方法
if (type && type.defaultProps) {
//如果當前組件中有默認的defaultProps則把當前組件的默認內容 定義到defaultProps中
var defaultProps = type.defaultProps
for (propName in defaultProps) {
if (props[propName] === undefined) {
//如果父組件中對應的值為undefined 則把默認值賦值賦值給props當作props的屬性
props[propName] = defaultProps[propName]
}
}
}
下面將檢測key
和ref
是否被賦值,如果有那麼就會執行defineKeyPropWarningGetter
和defineRefPropWarningGetter
兩個函數,隨後將一系列組裝好的數據傳入ReactElement
中
此處defineKeyPropWarningGetter
和defineRefPropWarningGetter
兩個函數的作用是讓 ref 和 key 在被獲取的時候報錯
ReactElement#
createElement()
最終調用的方法,其實ReactElement()
方法只是將傳入的一系列數據增加一些諸如type
、source
、self
等標記屬性然後直接返回一個 js 對象。
JSX 中的使用的類似 html 的節點,在 Babel 的幫助下,轉換為嵌套的 ReactElement
對象,這些信息對於後期構建應用的樹結構時非常重要的,而 React 通過提供這些類型的數據,來脫離平台的限制。