React 內部機制探祕 - React Component 和 Element(文末附彩蛋demo和原始碼)

LucasHC發表於2017-09-29

這篇文章比較偏基礎,但是對入門 React 內部機制和實現原理卻至關重要。算是為以後深入解讀的一個入門,如果您已經非常清楚:

React Component Render => JSX => React.createElement => Virtual Dom

的流程,可以直接略過此文。

谷歌工程師一個風騷的問題

在幾個月前,谷歌的前端開發專家 Tyler McGinnis 在其個人 twitter 賬號上釋出了 這樣一條推文,引發了對 React 元件的討論。

推文截圖
推文截圖

他丟擲來的問題是 :如上述程式碼,React 元件 Icon 直接出現在程式碼中,到底算什麼?

提供的選項有:

  • A. Component Declaration 元件宣告
  • B. Component Invocation 元件呼叫
  • C. Component Instantiation 元件例項化
  • D. Using a Component 單純地使用元件

有趣的是,參與回答的開發者中:

  • 有 15% 選擇了 A 項;
  • 有 8% 選擇了 B 項;
  • 有 45% 選擇了 C 項;
  • 有 32% 選擇了 D 項;

對 React 開發經驗豐富的前端工程師來說,這個問題其實很好理解。它的關鍵在於:真正明白 React Element 和 React Components,以及 JSX 抽象層是如連通 React 的。當然也需要明白一些淺顯的 React 內部工作機制。

這篇文章,就帶領大家研究一下這個 JSX 抽象層的奧祕和 React Reconciliation 過程。

React 和 React Element 到底是什麼?

讓我們回到最初,思考一下最原始的問題,React 到底是什麼?

簡而言之,

React is a library for building user interfaces.

React 是一個構建檢視層的類庫(框架...whatever...)。不管 React 本身如何複雜,不管其生態如何龐大,構建檢視始終是他的核心。記住這個資訊,我們即將進入今天的第一個概念 — React Element

簡單地說,React Element 描述了“你想”在螢幕上看到的事物。

抽象地說,React Element 元素是一個描述了 Dom Node 的物件。

請注意我的用詞 — “描述”,因為 React Element 並不是你在螢幕上看見的真實事物。相反地,他是一個描述真實事物的集合。存在的就是合理的,我們看來看看 React Element 存在的意義,以及為什麼會有這樣一個概念:

  • JavaScript 物件很輕量。用物件來作為 React Element,那麼 React 可以輕鬆的建立或銷燬這些元素,而不必去太擔心操作成本;
  • React 具有分析這些物件的能力,進一步,也具有分析虛擬 Dom 的能力。當改變出現時,(相比於真實 Dom)更新虛擬 Dom 的效能優勢非常明顯。

為了建立我們描述 Dom Node 的物件(或者 React Element),我們可以使用 React.createElement 方法:

const element = React.createElement( 
  'div', 
  {id: 'login-btn'}, 
  'Login'
)複製程式碼

這裡 React.createElement 方法接受三個引數:

  • 一個表述標籤名稱的字串 (div, span, etc.);
  • 當前 React Element 需要具有的屬性;
  • 當前 React Element 要表達的內容,或者一個子元素。

上面 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 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 值:

  • "button";
  • "div";
  • "p";
  • Button

當 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 這個老朋友了。

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!

PS:
作者Github倉庫知乎問答連結
歡迎各種形式交流。

相關文章