React原始碼學習——ReactElement.createElement

weixin_34185560發表於2018-01-25

最近在學習React的原始碼,從比較簡單的建立ReactElement開始學起,以下是今天要啃的原始碼,可能有些地方還不是很深入,相信隨著瞭解的增多,對react的理解也會更加深入:

ReactElement.createElement = function (type, config, children) {
  var propName;
  var props = {};
  var key = null;
  var ref = null;
  var self = null;
  var source = null;
  if (config != null) {
    if (hasValidRef(config)) {
      ref = config.ref;
    }
    if (hasValidKey(config)) {
      key = '' + config.key;
    }
    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    for (propName in config) {
      if (hasOwnProperty.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }
  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];
    }
    if ("development" !== 'production') {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  if ("development" !== 'production') {
    if (key || ref) {
      if (typeof props.$$typeof === 'undefined' || props.$$typeof !== REACT_ELEMENT_TYPE) {
        var displayName = typeof type === 'function' ? type.displayName || type.name || 'Unknown' : type;
        if (key) {
          defineKeyPropWarningGetter(props, displayName);
        }
        if (ref) {
          defineRefPropWarningGetter(props, displayName);
        }
      }
    }
  }
  return ReactElement(type, key, ref, self, source, ReactCurrentOwner.current, props);
};

How to trigger it ?

兩種方式可以觸發上面這個方法:

  1. 使用JSX建立元素
render() {
  return (
    <button className="button" type="button")}>Click</button>
  )
}
  1. 使用React.createElement建立元素
render() {
  var config = {
    className: 'button',
    type: 'button'
  };
  return React.createElement('button', config, 'Click');
}

JSX本質上是ReactJS建立元素的語法糖,它們都在做同一件事情,就是生成一個簡單的按鈕,babel會幫我們將JSX轉化成Javascript。

Parameters passed into the method

這個方法看起來是接收三個引數,不過了解Javascript函式機制的話,就知道其實不是的,不過我們先假裝它接收三個引數:

  1. type: 這個引數宣告要建立什麼型別的DOM元素,分兩種,一種是原生DOM,比如div span h1等等,傳入一個string,比如'div' 'span' 'h1'等等,即可正確的表示要建立的DOM型別。另一種是自定義的Web Component,比如MyComponent,此時需要引入這個元件,然後將元件作為第一個引數傳入。

The first part of a JSX tag determines the type of the React element.
Capitalized types indicate that the JSX tag is referring to a React component. These tags get compiled into a direct reference to the named variable, so if you use the JSX <Foo /> expression, Foo must be in scope.(出自ReactJS官方文件)

  1. config: 用來宣告元件的屬性列表。
  2. children: 用來在元件內建立子元素,例子中是Click,我們也可以建立更復雜的子元素,比如div等等。

Then, what happened?

  1. 首先,在config不為空的情況下,校驗是否有合法的ref屬性以及是否是合法的鍵值對。
function hasValidRef(config) {
  if ("development" !== 'production') {
    if (hasOwnProperty.call(config, 'ref')) {
      var getter = Object.getOwnPropertyDescriptor(config, 'ref').get;
      if (getter && getter.isReactWarning) {
        return false;
      }
    }
  }
  return config.ref !== undefined;
}

本菜鳥一直搞不懂問什麼要寫"development" !== 'production'這樣一句永遠為true的表示式(React原始碼中經常出現),歡迎大家評論解答_
首先判斷ref是否是config自身的屬性,而不是從原型鏈上繼承來的屬性,然後再判斷此ref是否有效。

function hasValidKey(config) {
  if ("development" !== 'production') {
    if (hasOwnProperty.call(config, 'key')) {
      var getter = Object.getOwnPropertyDescriptor(config, 'key').get;
      if (getter && getter.isReactWarning) {
        return false;
      }
    }
  }
  return config.key !== undefined;
}

key的判斷跟ref很類似。

  1. 裝載self和source
    我們的例子中config__self __source均為undefined,這兩個變數還沒有搞懂,歡迎大家留言。

  2. config中的自有而非從原型鏈繼承得來、非保留屬性(key ref __self __source)儲存到區域性變數props中。

  3. 前面提到過假裝只接受三個引數,其實可以接受多於三個引數的,那麼第四個、第五個引數要怎麼處理呢?這個方法中,它們被當做子元素來處理
    首先判斷子元素的長度是否為1,是的話直接將第三個引數放入props.children中,如果大於1, 那麼構建子元素陣列,將第三個、第四個、第五個...引數依次放入這個陣列中,然後將這個陣列賦值給props.children
    將引數指定的屬性設定好之後,開始處理指定元素型別帶有預設值的屬性,如果被設定預設值的屬性沒有被指定新值,那麼儲存預設值到props中。

  4. 接下來的一段程式碼,在props存在keyref的情況下,並且props.$$typeof為空或者不等於Symbol(react.element)或者60103時(即不是ReactElement),為props.key props.ref新增get屬性,get屬性指向一個函式,這個函式可以顯示初次的報警資訊。這一步顯然與第1步有聯絡,但仍然想不出在什麼情形下會顯示警告,大家知道的話可以告訴我~
    下面這段程式碼是REACT_ELEMENT_TYPE的賦值操作

var REACT_ELEMENT_TYPE = typeof Symbol === 'function' && Symbol['for'] && Symbol['for']('react.element') || 0xeac7;

Symbol是ES6新定義的一種基本資料型別:
MDN web docs: Symbol

感覺defineXXXPropWarningGetter就是為key ref屬性新增訪問控制,不知道對不對...(原諒React小白留下這麼多坑...),以下是程式碼:

function defineKeyPropWarningGetter(props, displayName) {
  var warnAboutAccessingKey = function () {
    if (!specialPropKeyWarningShown) {
      specialPropKeyWarningShown = true;
      "development" !== 'production' ? warning(false, '%s: `key` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://fb.me/react-special-props)', displayName) : void 0;
    }
  };
  warnAboutAccessingKey.isReactWarning = true;
  Object.defineProperty(props, 'key', {
    get: warnAboutAccessingKey,
    configurable: true
  });
}

function defineRefPropWarningGetter(props, displayName) {
  var warnAboutAccessingRef = function () {
    if (!specialPropRefWarningShown) {
      specialPropRefWarningShown = true;
      "development" !== 'production' ? warning(false, '%s: `ref` is not a prop. Trying to access it will result ' + 'in `undefined` being returned. If you need to access the same ' + 'value within the child component, you should pass it as a different ' + 'prop. (https://fb.me/react-special-props)', displayName) : void 0;
    }
  };
  warnAboutAccessingRef.isReactWarning = true;
  Object.defineProperty(props, 'ref', {
    get: warnAboutAccessingRef,
    configurable: true
  });
}
  1. 萬事俱備,現在可以正兒八經的建立ReactElement啦(構建ReactElement資料結構)
    首先給這個element打上react的標籤(REACT_ELEMENT_TYPE),然後將校驗合格的變數填充到element物件中,其中owner是ReactCurrentOwner.current,就是管理它的component, 並且定義_store.validated _self _source(是否可配置,是否可編輯,是否可列舉,值)
var ReactElement = function (type, key, ref, self, source, owner, props) {
  var element = {
    $$typeof: REACT_ELEMENT_TYPE,
    type: type,
    key: key,
    ref: ref,
    props: props,
    _owner: owner
  };

  if ("development" !== 'production') {
    element._store = {};
    if (canDefineProperty) {
      Object.defineProperty(element._store, 'validated', {
        configurable: false,
        enumerable: false,
        writable: true,
        value: false
      });
      Object.defineProperty(element, '_self', {
        configurable: false,
        enumerable: false,
        writable: false,
        value: self
      });
      Object.defineProperty(element, '_source', {
        configurable: false,
        enumerable: false,
        writable: false,
        value: source
      });
    } else {
      element._store.validated = false;
      element._self = self;
      element._source = source;
    }
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }
  return element;
};

相關文章