React源碼系列一之createElement

前言:使用react也有二年多了,一直停留在使用層次。雖然不少時候這樣是夠了。可是總以爲不深刻理解其背後是的實現邏輯,很難體會框架的精髓。最近會寫一些相關的一些文章,來記錄學習的過程。node

備註:react和react-dom源碼版本爲16.8.6 本文適合使用過React進行開發,並有必定經驗的人閱讀。

好了閒話少說,咱們一塊兒來看源碼吧
寫過react知道,咱們使用react編寫代碼都離不開webpackbabel,由於React要求咱們使用的是class定義組件,而且使用了JSX語法編寫HTML。瀏覽器是不支持JSX而且對於class的支持也很差,因此咱們都是須要使用webpack的jsx-loaderjsx的語法作一個轉換,而且對於ES6的語法和react的語法經過babelbabel/preset-reactbabel/env@babel/plugin-proposal-class-properties等進行轉義。不熟悉怎麼從頭搭建react的個人示例代碼就放在這。react

好了,咱們從一個最簡單實例demo來看react到底作了什麼webpack

一、createElement

下面是咱們的代碼git

import React from "react";
import ReactDOM from "react-dom";
ReactDOM.render(
    <h1 style={{color:'red'}} >11111</h1>, 
    document.getElementById("root")
);

這是頁面上的效果
圖片描述github

咱們如今看看在瀏覽器中的代碼是如何實現的:web

react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", {
  style: {
    color: 'red'
  }
}, "11111"), document.getElementById("root"));

最終通過編譯後的代碼是這樣的,發現本來的<h1>11111</h1>變成了一個react.createElement的函數,其中原生標籤的類型,內容都變成了參數傳入這個函數中.這個時候咱們大膽的猜想react.createElement接受三個參數,分別是元素的類型、元素的屬性、子元素。好了帶着咱們的猜測來看一下源碼。瀏覽器

咱們不難找到,源碼位置在位置 ./node_modules/react/umd/react.development.js:1941babel

function createElement(type, config, children) {
  var propName = void 0;

  // Reserved names are extracted
  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;
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (hasOwnProperty$1.call(config, propName) && !RESERVED_PROPS.hasOwnProperty(propName)) {
        props[propName] = config[propName];
      }
    }
  }

  // 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];
    }
    {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
  }

  // Resolve default props
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (props[propName] === undefined) {
        props[propName] = defaultProps[propName];
      }
    }
  }
  {
    if (key || ref) {
      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);
}

首先咱們來看一下它的三個參數
第一個type:咱們想一下這個type的可能取值有哪些?app

  • 第一種就是咱們上面寫的原生的標籤類型(例如h1div,span等);
  • 第二種就是咱們React組件了,就是這面這種App
class App extends React.Component {

    static defaultProps = {
        text: 'DEMO'
    }
    render() {
        return (<h1>222{this.props.text}</h1>)
    }
}

第二個config:這個就是咱們傳遞的一些屬性
第三個children:這個就是子元素,最開始咱們猜測就三個參數,其實後面看了源碼就知道這裏其實不止三個。框架

接下來咱們來看看react.createElement這個函數裏面會幫咱們作什麼事情。
一、首先會初始化一些列的變量,以後會判斷咱們傳入的元素中是否帶有有效的keyref的屬性,這兩個屬性對於react是有特殊意義的(key是能夠優化React的渲染速度的,ref是能夠獲取到React渲染後的真實DOM節點的),若是檢測到有傳入key,ref,__self__source這4個屬性值,會將其保存起來。

二、接着對傳入的config作處理,遍歷config對象,而且剔除掉4個內置的保留屬性(key,ref,__self,__source),以後從新組裝新的configprops。這個RESERVED_PROPS是定義保留屬性的地方。

var RESERVED_PROPS = {
      key: true,
      ref: true,
      __self: true,
      __source: true
    };

三、以後會檢測傳入的參數的長度,若是childrenLength等於1的狀況下,那麼就表明着當前createElement的元素只有一個子元素,那麼將內容賦值到props.children。那何時childrenLength會大於1呢?那就是當你的元素裏面涉及到多個子元素的時候,那麼children將會有多個傳入到createElement函數中。例如:

ReactDOM.render(
        <h1 style={{color:'red'}} key='22'>
            <div>111</div>
            <div>222</div>
        </h1>, 
        document.getElementById("root")
    );

編譯後是什麼樣呢?

react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(
    react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", {
      style: {
        color: 'red'
      },
      key: "22"
    }, 
    react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, "111"), 
    react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", null, "222")), 
    document.getElementById("root")
  );

這個時候react.createElement拿到的arguments.length就大於3了。也就是childrenLength大於1。這個時候咱們就遍歷把這些子元素添加到props.children中。
四、接着函數將會檢測是否存在defaultProps這個參數,由於如今的是一個最簡單的demo,並且傳入的只是原生元素,因此沒有defaultProps這個參數。那麼咱們來看下面的例子:

import React, { Component } from "react";
    import ReactDOM from "react-dom";
    class App extends Component {
        static defaultProps = {
            text: '33333'
        }
        render() {
            return (<h1>222{this.props.text}</h1>)
        }
    }
    ReactDOM.render(
        <App/>, 
        document.getElementById("root")
    );

編譯後的

var App =
    /*#__PURE__*/
    function (_Component) {
      _inherits(App, _Component);
    
      function App() {
        _classCallCheck(this, App);
    
        return _possibleConstructorReturn(this, _getPrototypeOf(App).apply(this, arguments));
      }
    
      _createClass(App, [{
        key: "render",
        value: function render() {
          return react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("h1", null, "222", this.props.text);
        }
      }]);
    
      return App;
    }(react__WEBPACK_IMPORTED_MODULE_0__["Component"]);
    
    _defineProperty(App, "defaultProps", {
      text: '33333'
    });
    
    react_dom__WEBPACK_IMPORTED_MODULE_1___default.a.render(
      react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement(App, null), 
      document.getElementById("root")
      );

發現傳入react.createElement的是一個App的函數,class通過babel轉換後會變成一個構造函數。有興趣能夠本身去看babel對於class的轉換,這裏就不解析轉換過程,總得來講就是返回一個App的構造函數傳入到react.createElement中.若是type傳的東西是個對象,且typedefaultProps這個東西而且props中對應的值是undefined,那就defaultProps的值也塞props裏面。這就是咱們組價默認屬性的由來。

五、 檢測keyref是否有賦值,若是有將會執行defineKeyPropWarningGetterdefineRefPropWarningGetter兩個函數。

function defineKeyPropWarningGetter(props, displayName) {
  var warnAboutAccessingKey = function () {
    if (!specialPropKeyWarningShown) {
      specialPropKeyWarningShown = true;
      warningWithoutStack$1(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);
    }
  };
  warnAboutAccessingKey.isReactWarning = true;
  Object.defineProperty(props, 'key', {
    get: warnAboutAccessingKey,
    configurable: true
  });
}

function defineRefPropWarningGetter(props, displayName) {
  var warnAboutAccessingRef = function () {
    if (!specialPropRefWarningShown) {
      specialPropRefWarningShown = true;
      warningWithoutStack$1(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);
    }
  };
  warnAboutAccessingRef.isReactWarning = true;
  Object.defineProperty(props, 'ref', {
    get: warnAboutAccessingRef,
    configurable: true
  });
}

我麼能夠看出這個二個方法就是給keyref添加了警告。這個應該只是在開發環境纔有其中isReactWarning就是上面判斷keyref是否有效的一個標記。
六、最後將一系列組裝好的數據傳入ReactElement函數中。

二、ReactElement

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
  };

  {
    element._store = {};
    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
    });
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};

其實裏面很是簡單,就是將傳進來的值都包裝在一個element對象中

  • $$typeof:其中REACT_ELEMENT_TYPE是一個常量,用來標識該對象是一個ReactElement
var hasSymbol = typeof Symbol === 'function' && Symbol.for;
var REACT_ELEMENT_TYPE = hasSymbol ? Symbol.for('react.element') : 0xeac7;

從代碼上看若是支持Symbol就會用Symbol.for方法建立一個keyreact.elementsymbol,不然就會返回一個0xeac7

  • type -> tagName或者是一個函數
  • key -> 渲染元素的key
  • ref -> 渲染元素的ref
  • props -> 渲染元素的props
  • _owner -> Record the component responsible for creating this element.(記錄負責建立此元素的組件,默認爲null)
  • _store -> 新的對象

_store中添加了一個新的對象validated(可寫入),
element對象中添加了_self_source屬性(只讀),最後凍結了element.propselement
這樣就解釋了爲何咱們在子組件內修改props是沒有效果的,只有在父級修改了props後子組件纔會生效

最後就將組裝好的element對象返回了出來,提供給ReactDOM.render使用。到這有關的主要內容咱們看完了。下面咱們來補充一下知識點

Object.freeze

Object.freeze方法能夠凍結一個對象,凍結指的是不能向這個對象添加新的屬性,不能修改其已有屬性的值,不能刪除已有屬性,以及不能修改該對象已有屬性的可枚舉性、可配置性、可寫性。該方法返回被凍結的對象。

const obj = {
    a: 1,
    b: 2
};

Object.freeze(obj);

obj.a = 3; // 修改無效

須要注意的是凍結中能凍結當前對象的屬性,若是obj中有一個另外的對象,那麼該對象仍是能夠修改的。因此React纔會須要凍結element和element.props。

if (Object.freeze) {
  Object.freeze(element.props);
  Object.freeze(element);
}
後續更多文章將在個人 github第一時間發佈,歡迎關注。
相關文章
相關標籤/搜索