1. 程式人生 > >React原始碼分析2 — 元件和物件的建立(createClass,createElement)

React原始碼分析2 — 元件和物件的建立(createClass,createElement)

1 元件的建立

React受大家歡迎的一個重要原因就是可以自定義元件。這樣的一方面可以複用開發好的元件,實現一處開發,處處呼叫,另外也能使用別人開發好的元件,提高封裝性。另一方面使得程式碼結構很清晰,元件間耦合減少,方便維護。ES5建立元件時,呼叫React.createClass()即可. ES6中使用class myComponent extends React.Component, 其實內部還是呼叫createClass建立元件。

元件建立我們可以簡單類比為Java中ClassLoader載入class。下面來分析下createClass的原始碼,我們省去了開發階段錯誤提示的相關程式碼,如propType的檢查。(if (“development” !== ‘production’) {}程式碼段都不進行分析了,這些只在開發除錯階段呼叫)

createClass: function (spec) {
    var Constructor = function (props, context, updater) {
      // 觸發自動繫結
      if (this.__reactAutoBindPairs.length) {
        bindAutoBindMethods(this);
      }

      // 初始化引數
      this.props = props;
      this.context = context;
      this.refs = emptyObject;  // 本元件物件的引用,可以利用它來呼叫元件的方法
this.updater = updater || ReactNoopUpdateQueue; // 呼叫getInitialState()來初始化state變數 this.state = null; var initialState = this.getInitialState ? this.getInitialState() : null; this.state = initialState; }; // 繼承父類 Constructor.prototype = new ReactClassComponent(); Constructor.prototype.constructor = Constructor; Constructor.prototype.__reactAutoBindPairs = []; injectedMixins.forEach(mixSpecIntoComponent.bind(null
, Constructor)); mixSpecIntoComponent(Constructor, spec); // 呼叫getDefaultProps,並掛載到元件類上。defaultProps是類變數,使用ES6寫法時更清晰 if (Constructor.getDefaultProps) { Constructor.defaultProps = Constructor.getDefaultProps(); } // React中暴露給應用呼叫的方法,如render componentWillMount。 // 如果應用未設定,則將他們設為null for (var methodName in ReactClassInterface) { if (!Constructor.prototype[methodName]) { Constructor.prototype[methodName] = null; } } return Constructor; },

createClass主要做的事情有

  1. 定義構造方法Constructor,構造方法中進行props,refs等的初始化,並呼叫getInitialState來初始化state
  2. 呼叫getDefaultProps,並放在defaultProps類變數上。這個變數不屬於某個單獨的物件。可理解為static 變數
  3. 將React中暴露給應用,但應用中沒有設定的方法,設定為null。

2 物件的建立

JSX中建立React元素最終會被babel轉譯為createElement(type, config, children), babel根據JSX中標籤的首字母來判斷是原生DOM元件,還是自定義React元件。如果首字母大寫,則為React元件。這也是為什麼ES6中React元件類名必須大寫的原因。如下面程式碼

<div className="title" ref="example">
    <span>123</span>    // 原生DOM元件,首字母小寫
    <ErrorPage title='123456' desc={[]}/>    // 自定義元件,首字母大寫
</div>

轉譯完後是

React.createElement(
        'div',    // type,標籤名,原生DOM物件為String
        {
            className: 'title',
            ref: 'example'
        },   // config,屬性
        React.createElement('span', null, '123'),   // children,子元素
        React.createElement(
            // type,標籤名,React自定義元件的type不為String.
            // _errorPage2.default為從其他檔案中引入的React元件
            _errorPage2.default,    
            {
                title: '123456',
                desc: []
            }
        )   // children,子元素
)

下面來分析下createElement的原始碼

ReactElement.createElement = function (type, config, children) {
  var propName;

  // 初始化引數
  var props = {};

  var key = null;
  var ref = null;
  var self = null;
  var source = null;

  // 從config中提取出內容,如ref key 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;

    // 提取出config中的prop,放入props變數中
    for (propName in config) {
      if (config.hasOwnProperty(propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }

  // 處理children,掛到props的children屬性下
  // 入參的前兩個為type和config,後面的就都是children引數了。故需要減2
  var childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    // 只有一個引數時,直接掛到children屬性下,不是array的方式
    props.children = children;
  } else if (childrenLength > 1) {
    // 不止一個時,放到array中,然後將array掛到children屬性下
    var childArray = Array(childrenLength);
    for (var i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    props.children = childArray;
  }

  // 取出元件類中的靜態變數defaultProps,並給未在JSX中設定值的屬性設定預設值
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }

  // 返回一個ReactElement物件
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
};

下面來看ReactElement原始碼

var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    // This tag allow us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // ReactElement物件上的四個變數,特別關鍵
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner
  };
    return element;
}

可以看到僅僅是給ReactElement物件內的成員變數賦值而已,不在贅述。

流程如如下

Markdown

3 元件物件初始化

在mountComponent()掛載元件中,會進行元件渲染,呼叫到instantiateReactComponent()方法。這個過程我們在React生命週期方法中再詳細講述,這裡有個大體瞭解即可。instantiateReactComponent()根據ReactElement中不同的type欄位,建立不同型別的元件物件。原始碼如下

// 初始化元件物件,node是一個ReactElement物件,是節點元素在React中的表示
function instantiateReactComponent(node) {
  var instance;

  var isEmpty = node === null || node === false;
  if (isEmpty) {
    // 空物件
    instance = ReactEmptyComponent.create(instantiateReactComponent);
  } else if (typeof node === 'object') {
    // 元件物件,包括DOM原生的和React自定義元件
    var element = node;

    // 根據ReactElement中的type欄位區分
    if (typeof element.type === 'string') {
      // type為string則表示DOM原生物件,比如div span等。可以參看上面babel轉譯的那段程式碼
      instance = ReactNativeComponent.createInternalComponent(element);
    } else if (isInternalComponentType(element.type)) {
      // 保留給以後版本使用,此處暫時不會涉及到
      instance = new element.type(element);
    } else {
      // React自定義元件
      instance = new ReactCompositeComponentWrapper(element);
    }
  } else if (typeof node === 'string' || typeof node === 'number') {
    // 元素是一個string時,對應的比如<span>123</span> 中的123
    // 本質上它不是一個ReactElement,但為了統一,也按照同樣流程處理,稱為ReactDOMTextComponent
    instance = ReactNativeComponent.createInstanceForText(node);
  } else {
    // 無處理
  }

  // 初始化引數,這兩個引數是DOM diff時用到的
  instance._mountIndex = 0;
  instance._mountImage = null;

  return instance;
}

故我們可以看到有四種建立元件元素的方式,同時對應四種ReactElement

  1. ReactEmptyComponent.create(), 建立空物件ReactDOMEmptyComponent
  2. ReactNativeComponent.createInternalComponent(), 建立DOM原生物件 ReactDOMComponent
  3. new ReactCompositeComponentWrapper(), 建立React自定義物件ReactCompositeComponent
  4. ReactNativeComponent.createInstanceForText(), 建立文字物件 ReactDOMTextComponent

下面分別分析這幾種物件,和建立他們的過程。

ReactDOMEmptyComponent

由ReactEmptyComponent.create()建立,最終生成ReactDOMEmptyComponent物件,原始碼如下

var emptyComponentFactory;

var ReactEmptyComponentInjection = {
  injectEmptyComponentFactory: function (factory) {
    emptyComponentFactory = factory;
  }
};

var ReactEmptyComponent = {
  create: function (instantiate) {
    return emptyComponentFactory(instantiate);
  }
};

ReactEmptyComponent.injection = ReactEmptyComponentInjection;

ReactInjection.EmptyComponent.injectEmptyComponentFactory(function (instantiate) {
  // 前面比較繞,關鍵就是這句話,建立ReactDOMEmptyComponent物件
   return new ReactDOMEmptyComponent(instantiate);
});

// 各種null,就不分析了
var ReactDOMEmptyComponent = function (instantiate) {
  this._currentElement = null;
  this._nativeNode = null;
  this._nativeParent = null;
  this._nativeContainerInfo = null;
  this._domID = null;
};

ReactDOMComponent

由ReactNativeComponent.createInternalComponent()建立。這裡注意原生元件不代表是DOM元件,而是React封裝過的Virtual DOM物件。React並不直接操作原生DOM。

大家可以自己看ReactDOMComponent的原始碼。重點看下ReactDOMComponent.Mixin

ReactDOMComponent.Mixin = {
  mountComponent: function (transaction, nativeParent, nativeContainerInfo, context) {},
  _createOpenTagMarkupAndPutListeners: function (transaction, props){},
  _createContentMarkup: function (transaction, props, context) {},
  _createInitialChildren: function (transaction, props, context, lazyTree) {}
  receiveComponent: function (nextElement, transaction, context) {},
  updateComponent: function (transaction, prevElement, nextElement, context) {},
  _updateDOMProperties: function (lastProps, nextProps, transaction) {},
  _updateDOMChildren: function (lastProps, nextProps, transaction, context) {},
  getNativeNode: function () {},
  unmountComponent: function (safely) {},
  getPublicInstance: function () {}
}

其中暴露給外部的比較關鍵的是mountComponent,receiveComponen, updateComponent,unmountComponent。他們會引發React生命週期方法的呼叫,下一節再講。

ReactCompositeComponent

由new ReactCompositeComponentWrapper()建立,重點看下ReactCompositeComponentMixin

var ReactCompositeComponentMixin = {
  // new對應的方法,建立ReactCompositeComponent物件
  construct: function(element) {},
  mountComponent,   // 初始掛載元件時被呼叫,僅一次
  performInitialMountWithErrorHandling, // 和performInitialMount相近,只是多了錯誤處理
  performInitialMount,  // 執行mountComponent的渲染階段,會呼叫到instantiateReactComponent,從而進入初始化React元件的入口
  getNativeNode,
  unmountComponent, // 解除安裝元件,記憶體釋放等工作
  receiveComponent,
  performUpdateIfNecessary,
  updateComponent,  // setState後被呼叫,重新渲染元件
  attachRef,    // 將ref指向元件物件,這樣我們就可以利用它呼叫物件內的方法了
  detachRef,    // 將元件的引用從全域性物件refs中刪掉,這樣我們就不能利用ref找到元件物件了
  instantiateReactComponent,    // 初始化React元件的入口,在mountComponent時的渲染階段會被呼叫
}

ReactDOMTextComponent

由ReactNativeComponent.createInstanceForText()建立,我們也不細細分析了,主要入口程式碼如下,大家可以自行分析。

var ReactDOMTextComponent = function (text) {
  this._currentElement = text;
  this._stringText = '' + text;
};

_assign(ReactDOMTextComponent.prototype, {
  mountComponent,
  receiveComponent,
  getNativeNode,
  unmountComponent
}