React原始碼分析 - 元件初次渲染

_zon_發表於2018-02-25

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'
      style={{ 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'
      style={{ 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元件初次渲染如下。

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

react

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

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

參考資料

相關文章