這篇文章比較偏基礎,可是對入門 React 內部機制和實現原理卻相當重要。算是爲之後深刻解讀的一個入門,若是您已經很是清楚:javascript
React Component Render => JSX => React.createElement => Virtual Dom 前端
的流程,能夠直接略過此文。java
在幾個月前,谷歌的前端開發專家 Tyler McGinnis 在其我的 twitter 帳號上發佈了 這樣一條推文,引起了對 React 組件的討論。react
他拋出來的問題是 :如上述代碼,React 組件 Icon 直接出如今代碼中,到底算什麼?git
提供的選項有:github
有趣的是,參與回答的開發者中:redux
對 React 開發經驗豐富的前端工程師來講,這個問題其實很好理解。它的關鍵在於:真正明白 React Element 和 React Components,以及 JSX 抽象層是如連通 React 的。固然也須要明白一些淺顯的 React 內部工做機制。bash
這篇文章,就帶領你們研究一下這個 JSX 抽象層的奧祕和 React Reconciliation 過程。babel
讓咱們回到最初,思考一下最原始的問題,React 究竟是什麼?前端工程師
簡而言之,
React is a library for building user interfaces.
React 是一個構建視圖層的類庫(框架...whatever...)。無論 React 自己如何複雜,無論其生態如何龐大,構建視圖始終是他的核心。記住這個信息,咱們即將進入今天的第一個概念 — React Element。
簡單地說,React Element 描述了「你想」在屏幕上看到的事物。
抽象地說,React Element 元素是一個描述了 Dom Node 的對象。
請注意個人用詞 — 「描述」,由於 React Element 並非你在屏幕上看見的真實事物。相反地,他是一個描述真實事物的集合。存在的就是合理的,咱們看來看看 React Element 存在的意義,以及爲何會有這樣一個概念:
爲了建立咱們描述 Dom Node 的對象(或者 React Element),咱們可使用 React.createElement 方法:
const element = React.createElement(
'div',
{id: 'login-btn'},
'Login'
)複製代碼
這裏 React.createElement 方法接受三個參數:
上面 React.createElement 方法調用以後,會返回一個 javascript 對象:
{
type: 'div',
props: {
children: 'Login',
id: 'login-btn'
}
}複製代碼
接着當咱們使用 ReactDOM.render 方法,這才渲染到真實 DOM 之上時,就會獲得:
<div id='login-btn'>Login</div>複製代碼
而這個纔是真實的 Dom 節點。
到目前爲止,並無什麼很難理解的概念。
這篇文章咱們開篇就介紹了 React Element,而並非像官網或者學習資料上來就介紹 React Component,我相信你理解了 React Element,理解 React Component 就是天然而然的事情了。
在真正開發時,咱們並不直接使用 React.createElement,這樣作簡直太無聊了,每一個組件都這樣寫必定會瘋掉的。這時候就出現了 React Component,即 React 組件。
A component is a function or a Class which optionally accepts input and returns a React element.
沒錯,組件就是一個函數或者一個 Class(固然 Class 也是 function),它根據輸入參數,並最終返回一個 React Element,而不須要咱們直接手寫無聊的 React Element。
因此說,實際上咱們使用了 React Component 來生成 React Element,這對於開發體驗的提高無疑是巨大的。
這裏剖出一個思考題:全部 React Component 都須要返回 React Element 嗎?顯然是不須要的,那麼 return null; 的 React 組件有存在的意義嗎,它能完成並實現哪些巧妙的設計和思想?(請關注做者,下篇文章將會專門進行分析、講解)
接下來,請看這樣一段代碼:
function Button ({ onLogin }) {
return React.createElement(
'div',
{id: 'login-btn', onClick: onLogin},
'Login'
)
}複製代碼
咱們定義了一個 Button 組件,它接收 onLogin 參數,並返回一個 React Element。注意 onLogin 參數是一個函數,並最終像 id:'login-btn' 同樣成爲了這個 React Element 的屬性。
直到目前,咱們見到了一個 React Element type 爲 HTML 標籤(「span」, 「div」, etc)的狀況。事實上,咱們也能夠傳遞另外一個 React Element :
const element = React.createElement(
User,
{name: 'Lucas'},
null
)複製代碼
注意此時 React.createElement 第一個參數是另外一個 React Element,這與 type 值爲 HTML 標籤的狀況不盡相同,當 React 發現 type 值爲一個 class 或者函數時,它就會先看這個 class 或函數會返回什麼樣的 Element,併爲這個 Element 設置正確的屬性。
React 會一直不斷重複這個過程(有點相似遞歸),直到沒有 「createElement 調用 type 值爲 class 或者 function」 的狀況。
咱們結合代碼再來體會一下:
function Button ({ addFriend }) {
return React.createElement(
"button",
{ onClick: addFriend },
"Add Friend"
)
}
function User({ name, addFriend }) {
return React.createElement(
"div",
null,
React.createElement( "p", null, name ),
React.createElement(Button, { addFriend })
)
}複製代碼
上面有兩個組件:Button 和 User,User 描述的 Dom 是一個 div 標籤,這個 div 內,又存在一個 p 標籤,這個 p 標籤展現了用戶的 name;還存在一個 Button。
如今咱們來看 User 和 Button 中,React.createElement 返回狀況:
function Button ({ addFriend }) {
return {
type: 'button',
props: {
onClick: addFriend,
children: 'Add Friend'
}
}
}
function User ({ name, addFriend }) {
return {
type: 'div',
props: {
children: [{
type: 'p',
props: { children: name }
},
{
type: Button,
props: { addFriend }
}]
}
}
}複製代碼
你會發現,上面的輸出中,咱們發現了四種 type 值:
當 React 發現 type 是 Button 時,它會查詢這個 Button 組件會返回什麼樣的 React Element,並賦予正確的 props。
直到最終,React 會獲得完整的表述 Dom 樹的對象。在咱們的例子中,就是:
{
type: 'div',
props: {
children: [{
type: 'p',
props: { children: 'Tyler McGinnis' }
},
{
type: 'button',
props: {
onClick: addFriend,
children: 'Add Friend'
}
}]
}
}複製代碼
React 處理這些邏輯的過程就叫作 reconciliation,那麼「這個過程(reconciliation)在什麼時候被觸發呢?」
答案固然就是每次 setState 或 ReactDOM.render 調用時。之後的分析文章將會更加詳細的說明。
好吧,再回到 Tyler McGinnis 那個風騷的問題上。
此時咱們具有回答這個問題的一切知識了嗎?稍等等,我要引出 JSX 這個老朋友了。
在 React Component 編寫時,相信你們都在使用 JSX 來描述虛擬 Dom。固然,反過來講,React 其實也能夠脫離 JSX 而存在。
文章開頭部分,我提到 「不常被咱們提起的 JSX 抽象層是如何聯通 React 的?」 答案很簡單,由於 JSX 老是被編譯成爲 React.createElement 而被調用。通常 Babel 爲咱們作了 JSX —> React.createElement 這件事情。
再看來先例:
function Button ({ addFriend }) {
return React.createElement(
"button",
{ onClick: addFriend },
"Add Friend"
)
}
function User({ name, addFriend }) {
return React.createElement(
"div",
null,
React.createElement( "p", null, name),
React.createElement(Button, { addFriend })
)
}複製代碼
對應咱們總在寫的 JSX 用法:
function Button ({ addFriend }) {
return (
<button onClick={addFriend}>Add Friend</button>
)
}
function User ({ name, addFriend }) {
return (
<div>
<p>{name}</p>
<Button addFriend={addFriend}/>
</div>
)
}複製代碼
就是一個編譯產出的差異。
那麼,請你來回答「Icon 組件單獨出現表明了什麼?」
Icon 在 JSX 被編譯以後,就有:
React.createElement(Icon, null)複製代碼
你問我怎麼知道這些編譯結果的?
或者
你想知道你編寫的 JSX 最終編譯成了什麼樣子?
我寫了一個小工具,進行對 JSX 的實時編譯,放在 Github倉庫中,它使用起來是這樣子的:
平臺一分爲二,左邊能夠寫 JSX,右邊實時展示其編譯結果:
以及:
這個工具最核心的代碼其實就是使用 babel 進行編譯:
let code = e.target.value;
try {
this.setState({
output: window.Babel.transform(code, {presets: ['es2015', 'react']})
.code,
err: ''
})
}
catch(err) {
this.setState({err: err.message})
}複製代碼
感興趣的讀者能夠去 GitHub 倉庫參看源碼。
其實不論是 JSX 仍是 React Element、React Component 這些概念,都是你們在開發中每天接觸到的。有的開發者也許能上手作項目,可是並無深刻理解其中的概念,更沒法真正掌握 React 核心思想。
這些內容其實比較基礎,但同時又很關鍵,對於後續理解 React/Preact 源碼相當重要。在這個基礎上,我會更新更多更加深刻的類 React 實現原理剖析,感興趣的讀者能夠關注。
個人其餘幾篇關於React技術棧的文章:
Happy Coding!