背景
為了讓大家深刻理解 JSX 的含義。有必要簡單介紹了一下 JSX 稍微底層的運作原理,這樣大家可以更加深刻理解 JSX 到底是什麼東西,為什麼要有這種語法,它是經過怎麼樣的轉化變成頁面的元素的。
思考一個問題:如何用 JavaScript 物件來表現一個 DOM 元素的結構,舉個例子:
<div class='box' id='content'>
<div class='title'>Hello</div>
<button>Click</button>
</div>
複製程式碼
每個 DOM 元素的結構都可以用 JavaScript 的物件來表示。你會發現一個 DOM 元素包含的資訊其實只有三個:標籤名,屬性,子元素。
所以其實上面這個 HTML 所有的資訊我們都可以用合法的 JavaScript 物件來表示:
{
tag: 'div',
attrs: { className: 'box', id: 'content'},
children: [
{
tag: 'div',
arrts: { className: 'title' },
children: ['Hello']
},
{
tag: 'button',
attrs: null,
children: ['Click']
}
]
}
複製程式碼
你會發現,HTML 的資訊和 JavaScript 所包含的結構和資訊其實是一樣的,我們可以用 JavaScript 物件來描述所有能用 HTML 表示的 UI 資訊。但是用 JavaScript 寫起來太長了,結構看起來又不清晰,用 HTML 的方式寫起來就方便很多了。
於是 React.js 就把 JavaScript 的語法擴充套件了一下,讓 JavaScript 語言能夠支援這種直接在 JavaScript 程式碼裡面編寫類似 HTML 標籤結構的語法,這樣寫起來就方便很多了。編譯的過程會把類似 HTML 的 JSX 結構轉換成 JavaScript 的物件結構。
編譯前程式碼
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import './index.css'
class Header extends Component {
render () {
return (
<div>
<h1 className='title'>React 小書</h1>
</div>
)
}
}
ReactDOM.render(
<Header />,
document.getElementById('root')
)
複製程式碼
經過編譯以後會變成:
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import './index.css'
class Header extends Component {
render () {
return (
React.createElement(
"div",
null,
React.createElement(
"h1",
{ className: 'title' },
"React 小書"
)
)
)
}
}
ReactDOM.render(
React.createElement(Header, null),
document.getElementById('root')
);
複製程式碼
React.createElement 會構建一個 JavaScript 物件來描述你 HTML 結構的資訊,包括標籤名、屬性、還有子元素等。這樣的程式碼就是合法的 JavaScript 程式碼了。所以使用 React 和 JSX 的時候一定要經過編譯的過程。
這裡再重複一遍:所謂的 JSX 其實就是 JavaScript 物件。每當在 JavaScript 程式碼中看到這種 JSX 結構的時候,腦子裡面就可以自動做轉化,這樣對你理解 React.js 的元件寫法很有好處。
實現
注意到的是我們的JSX最終轉化成為的是React.createElement這個方法:
第一個引數是字串型別或者元件或者symbol,
代表的是標籤元素, 如div, span
classComponent或者是functional Component,
原生提供的Fragment, AsyncMode等, 會被特殊處理
複製程式碼
第二個引數是一個物件型別, 代表標籤的屬性, id, class
其餘的引數代表的是children,不涉及grand-children,當子節點中有孫節點的時候, 再遞迴使用React.createElement方法
const App = () => {
return <div id="app" key="key">
<section>
<img />
</section>
<span>span</span>
</div>
}
"use strict";
var App = function App() {
return React.createElement(
"div",
{id: "app",key: "key"},
React.createElement("section", null,
React.createElement("img", null)
),
React.createElement("span", null, "span"));
};
複製程式碼
當第一個引數是元件的時候,第一個引數是作為變數傳入的, 可以想像的是, React.createElement內部有一個簡單的判斷, 如果傳入的是元件的話, 內部還會呼叫React.createElement方法
const Child = () => {
return <div>Child</div>
}
const App = () => {
return <div id="app">
<Child />
</div>
}
"use strict";
var Child = function Child() {
return React.createElement("div", null, "Child");
};
var App = function App() {
return React.createElement(
"div",
{id: "app"},
React.createElement(Child, null)); //這裡
}
}
複製程式碼
需要注意的是如果元件開頭是一個小寫的話, 會被解析成簡單的字串,在執行的時候就會報錯
我們的createElement方法定義在packages/src/ReactElement.js
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
// ref和key和其他props不同, 進行單獨處理
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
}
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
//將屬性名掛載到props上
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
//第三個及以上的引數都被看作是子節點
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
//當陣列長度確定時,這種方式比push要節省記憶體
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
props.children = childArray;
// merge defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
複製程式碼
ReactElement定義如下
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// 每個react element的$$typeof屬性都是REACT_ELEMENT_TYPE
$$typeof: REACT_ELEMENT_TYPE, // react element的內建屬性
type: type,
key: key,
ref: ref,
props: props,
_owner: owner, //建立該元素的component
};
return element;
};
複製程式碼
總的來說就是返回一個react element, 該element帶有props, refs, type
所以可以總結一下從 JSX 到頁面到底經過了什麼樣的過程:
第一個原因是,當我們拿到一個表示 UI 的結構和資訊的物件以後,不一定會把元素渲染到瀏覽器的普通頁面上,我們有可能把這個結構渲染到 canvas 上,或者是手機 App 上。所以這也是為什麼會要把 react-dom 單獨抽離出來的原因,可以想象有一個叫 react-canvas 可以幫我們把 UI 渲染到 canvas 上,或者是有一個叫 react-app 可以幫我們把它轉換成原生的 App(實際上這玩意叫 ReactNative)。
第二個原因是,有了這樣一個物件。當資料變化,需要更新元件的時候,就可以用比較快的演算法操作這個 JavaScript 物件,而不用直接操作頁面上的 DOM,這樣可以儘量少的減少瀏覽器重排,極大地優化效能。這個在以後的章節中我們會提到。