標籤:

React源碼分析 - 組件初次渲染

React也寫了有一段時間了,不了解下ta的原理都不好意思和別人說自己會React...所以看了一些源碼分析的文章,自己也擼了一遍React的源碼【真是有點繞】,算是搞明白了React的原理。

但是最近給一個妹紙解釋React原理的時候,把她說蒙圈了...很受傷,本著面向妹紙編程的原則,決定還是要寫成文章,再給她看看。

【本文基於 React v15.0.0】

【源碼分析部分只保留production環境下的核心功能的代碼】

基本概念

在react的源碼中有三個概念需要先分清楚:

  • ReactElement
  • ReactComponent
  • ReactClass

ReactElement

ReactElement是描述react中的虛擬的DOM節點的對象,ReactElement主要包含了這個DOM節點的類型(type)、屬性(props)和子節點(children)。ReactElement只是包含了DOM節點的數據,還沒有注入對應的一些方法來完成React框架的功能。

ReactElement通過React.createElement來創建,使用jsx語法的表達式,也會被babel(react的媽媽Facebook在react剛出生的時候是有提供自己的編譯器的,但是Babel之後成為了社區主要的jsx語法編譯的工具)編譯成對應的調用React.createElement的形式。

在Babel官網上實驗一下比較清楚:

const React = require(react);const ReactDOM = require(react-dom);const View = ( <div className=wrapper--outer > <div className=wrapper1--inner stylex={{ color: #38f }} > hello world </div> <div className=wrapper2--inner > hello world </div> <Hello /> <Inner text="heiheihei"> <div>yoyoyo</div> </Inner> </div>);ReactDOM.render(<View />, document.getElementById(app));

Babel編譯後:

use strict;var React = require(react);var ReactDOM = require(react-dom);var View = React.createElement( div, { className: wrapper--outer }, React.createElement( div, { className: wrapper1--inner, style: { color: #38f } }, hello world ), React.createElement( div, { className: wrapper2--inner }, hello world ), React.createElement(Hello, null), React.createElement( Inner, { text: heiheihei }, React.createElement( div, null, yoyoyo ) ));ReactDOM.render(React.createElement(View, null), document.getElementById(app));

可以看到在jsx文件中的html寫法的表達式都會被編譯成調用React.createElement的形式。我們稱呼jsx裡面的為React的DOM標籤的話,如果DOM標籤的首字母為大寫的時候,這個標籤(類 => 自定義組件類, 函數 => 無狀態組件)則會被作為參數傳遞給createElement;如果DOM標籤的首字母為小寫,則將標籤名(div, span, a 等html的 DOM標籤)以字元串的形式傳給createElement;如果是字元串或者空的話,則直接將字元串或者null當做參數傳遞給createElement。

React.createElement的源碼(具體解釋看注釋):

ReactElement.createElement = function (type, config, children) { var propName; // Reserved names are extracted var props = {}; var key = null; var ref = null; var self = null; var source = null; // 將參數賦給props對象 if (config != null) { ref = config.ref === undefined ? null : config.ref; key = config.key === undefined ? null : + config.key; self = config.__self === undefined ? null : config.__self; source = config.__source === undefined ? null : config.__source; // Remaining properties are added to a new props object for (propName in config) { // 跳過React保留的參數 if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) { props[propName] = config[propName]; } } } // 將子元素按照順序賦給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); for (var i = 0; i < childrenLength; i++) { childArray[i] = arguments[i + 2]; } props.children = childArray; } // 對於默認的參數,判斷是否有傳入值,有的話直接將參數和對應的值賦給props,否則將參數和參數默認值賦給props // Resolve default props if (type && type.defaultProps) { var 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);};var ReactElement = function (type, key, ref, self, source, owner, props) { var element = { // Symbol類型的tag唯一標示這個對象是一個React Element類型 // This tag allow us to uniquely identify this as a React Element $$typeof: REACT_ELEMENT_TYPE, // Built-in properties that belong on the element type: type, key: key, ref: ref, props: props, // Record the component responsible for creating this element. _owner: owner }; return element;};

createElement基本沒做什麼特別的處理,返回了一個React Element對象,所以上述的栗子View最終是一個多層級的大對象(簡化如下):

const View = ( <div className=wrapper--outer > <div className=wrapper1--inner stylex={{ color: #38f }} > hello world </div> <div className=wrapper2--inner > hello world {null} </div> <Hello /> <Inner text="heiheihei"> <div>yoyoyo</div> </Inner> </div>);---------------{ type: div, props: { className: wrapper--outer }, children: [ { type: div, props: { className: wrapper1--inner, style: { color: #38f } }, children: hello world }, { type: div, props: { className: wrapper2-inner }, children: [ hello world, null ] }, { type: Hello, props: null }, { type: Inner, props: { text: heiheihei }, children: [ { type: div, props: null, children: yoyoyo } ] } ]}

這樣一個對象只是保存了DOM需要的數據,並沒有對應的方法來實現React提供給我們的那些功能和特性。ReactElement主要分為DOM Elements和Component Elements兩種,我們稱這樣的對象為ReactElement。

ReactComponent

ReactComponent是基於ReactElement創建的一個對象,這個對象保存了ReactElement的數據的同時注入了一些方法,這些方法可以用來實現我們熟知的那些React的特性。

ReactClass

ReactClass就是我們在寫React的時候extends至React.Component類的自定義組件的類,如上述中的View和Inner,ReactClass實例化後調用render方法可返回ReactElement。

組件初次渲染

對於擼源碼個人的習慣是代碼少的話,直接看就是了;代碼量大、複雜的話可以先看些文章,大致了解重點,(過一遍源碼,這個看個人興趣,最有效的還是打斷點執行查看實際運行的流程)然後挑幾個代表性的case打斷點單步執行把執行過程再過一遍就好了。

React的源碼比較繞,用了很多的依賴注入的方式去定義方法,基本上當你不清楚一個方法什麼時候定義的時候,ta可能就是在

(ReactMount.js)ReactDefaultInjection.inject();

中注入的。

這裡需要注意的是在React中主要有四類組件:

  • Empty Component(空元素)
  • Text Component(文本or數字)
  • DOM Component(DOM元素)
  • Composite Component(自定義組件元素)

根據輸入的ReactElement的type的類型的不同instantiateReactComponent方法會返回不同類型的Component。

在介紹了上面的一些需要了解的基本概念後,用流程圖來表示React組件初次渲染如下。

  • 藍色是輸入輸出
  • 橙色是一些重要的中間變數
  • 黑色調用到的函數

最終通過_mountImageIntoNode一次性將之前遞歸生成的markup渲染成真實的DOM。

該圖省略了事務、事件機制和生命周期等方面的內容,這些會在之後的文章單獨介紹。

參考資料

  • 深入理解react(源碼分析)
  • 深入理解React源碼 - 首次渲染

推薦閱讀:

前端知識 | CSS小技巧-自適應橢圓
前端日刊-2018.02.04
前端日刊-2018.02.09
前端日刊-2018.01.24
前端日刊-2018.01.21

TAG:前端開發 |