React@15.6.2源碼解析---從 ReactDOM.render 到頁面渲染(1)ReactMount

之前介紹了React16.8版本的React公用API,本著學習最新版的React的想法,但是敗在了Fiber的陣下,還有回過頭來寫搞明白React15的源碼,畢竟從15到16是一次重大的更新。本文中React源碼版本為 15.6.2 ,望各位看官找準版本號,不同的版本還是有著細微的區(qū)別的

值得一提的是,在閱讀源碼時,在Chrome中打斷點是一個很好的操作,可以了解到函數(shù)的調用棧,變量的值,一步一步的調試還可以了解整個執(zhí)行的流程,一邊調試一邊記錄著流程一邊在加以理解一邊感慨這神乎其技的封裝。

博客會同步到github上,這樣也算是有了開源的項目。歡迎各位看官指教!

準備步驟

首先需要安裝 React@15.6.2, ReactDOM@15.6.2 ,其次搭建webpack打包,因為必不可少的需要console.log啥的,另外需要babel的配置,babel6 babel7倒是無所謂,關鍵是可以解析我們的jsx語法。

示例代碼

import React from 'react';
import ReactDom from 'react-dom';

class App extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            name: 'Hello World'
        }
    }
    componentWillMount() {
        console.log('component will mount');
        this.setState({
            name: 'Hello CHina'
        })
    }
    componentDidMount() {
        console.log('component did mount');
        this.setState({
            name: 'Hello CHange'
        })
    }
    componentWillReceiveProps(nextProps) {
        console.log('component will receive props');
    }
    componentWillUpdate(nextProps, nextState) {
        console.log('component will updates');
    }
    componentDidUpdate(prevProps, prevState){
        console.log('component Did Update');
    };
    render() {
        console.log('render');
        return (
            <div>
                { this.state.name }
            </div>
        )
    }
};

ReactDom.render(
    <App>
        <div>Hello World</div>
    </App>,
    document.getElementById('root')
);

本片博客就是基于該代碼進行調試的,將這段代碼使用babel轉碼之后的結果為

// 主要代碼段
var App =
  /*#__PURE__*/
  function (_React$Component) {
    _inherits(App, _React$Component);

    function App(props) {
      var _this;

      _classCallCheck(this, App);

      _this = _possibleConstructorReturn(this, _getPrototypeOf(App).call(this, props));
      _this.state = {
        name: 'Hello World'
      };
      return _this;
    }

    _createClass(App, [{
      key: "componentWillMount",
      value: function componentWillMount() {
        console.log('component will mount');
        this.setState({
          name: 'Hello CHina'
        });
      }
    }, {
      key: "componentDidMount",
      value: function componentDidMount() {
        console.log('component did mount');
        this.setState({
          name: 'Hello CHange'
        });
      }
    }, {
      key: "componentWillReceiveProps",
      value: function componentWillReceiveProps(nextProps) {
        console.log('component will receive props');
      }
    }, {
      key: "componentWillUpdate",
      value: function componentWillUpdate(nextProps, nextState) {
        console.log('component will updates');
      }
    }, {
      key: "componentDidUpdate",
      value: function componentDidUpdate(prevProps, prevState) {
        console.log('component Did Update');
      }
    }, {
      key: "render",
      value: function render() {
        console.log('render');
        return _react["default"].createElement("div", null, this.state.name);
      }
    }]);

    return App;
  }(_react["default"].Component);

;

_reactDom["default"].render(_react["default"].createElement(App, null, _react["default"].createElement("div", null, "Hello World")), document.getElementById('root'));

一個立即執(zhí)行函數(shù),返回一個名為App的構造函數(shù),內部的componentWillMount render等方法,等會通過Object.defineProperty方法添加到App的原型鏈中。之后使用React.createElementApp轉換為ReactElement對象傳入到ReactDOM.render

看源碼需要扎實的Js基礎,原型鏈、閉包、this指向、模塊化、Object.defineProperty等常用的方法都是必須提前掌握的。

ReactDOM.render

在引入ReactDOM.js文件的時候,從上往下仔細看會發(fā)現(xiàn)有這么一行代碼是在引入的時候被執(zhí)行了ReactDefaultInjection.inject();,這個ReactDefaultInjection調用了其內部的一個inject方法,主要目的是進行一次全局的依賴注入,本博主一開始光注意著研究ReactDOM.render了,漏了這一句,導致后面有的東西很迷,所以在這提個醒,在引入一個文件時,文件內部有的函數(shù)是沒有被導出的反而是在引入文件時直接執(zhí)行的。這個inject具體的代碼后面用到時會進行詳細的介紹。

下面就是ReactDOM文件的代碼了

/* 各種文件的引入 */

// 執(zhí)行依賴注入
ReactDefaultInjection.inject();

// ReactDOM對象
var ReactDOM = {
  findDOMNode: findDOMNode,
  render: ReactMount.render,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  version: ReactVersion,

  /* eslint-disable camelcase */
  unstable_batchedUpdates: ReactUpdates.batchedUpdates,
  unstable_renderSubtreeIntoContainer: renderSubtreeIntoContainer
  /* eslint-enable camelcase */
};

/* 雜七雜八的東西 */

那么實質上ReactDOM.render方法就是ReactMount.render方法,ReactMount文件可以說是render的入口了,是一個極其重要的文件。當然ReactDOM兩萬多行代碼,重要的文件一大堆。。。。

ReactMount

還是一樣的,從上往下看仔細看,不要去找關鍵詞ReactMount,一旦找關鍵詞會錯過很多細節(jié)。一旦錯過了那么導致的結局就是臥槽,這個東西什么時候被賦值了,臥槽,這個屬性哪里來的尷尬局面。所以再一次強調,打斷點的好處。Chrome斷點,??

那么你會發(fā)現(xiàn),有這么一個構造函數(shù)

/**
 * Temporary (?) hack so that we can store all top-level pending updates on
 * composites instead of having to worry about different types of components
 * here.
 */
var topLevelRootCounter = 1;
var TopLevelWrapper = function () {
  this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.isReactComponent = {};
if (process.env.NODE_ENV !== 'production') {
  TopLevelWrapper.displayName = 'TopLevelWrapper';
}
TopLevelWrapper.prototype.render = function () {
  return this.props.child;
};
TopLevelWrapper.isReactTopLevelWrapper = true;

這個TopLevelWrapper就是整個組件的最頂層,我們調用ReactDOM.render時,傳遞的參數(shù)被這個構造函數(shù)給包裹起來。

// ReactMount.js

 /**
   *
   * @param {ReactElement} nextElement Component element to render.
   * @param {DOMElement} container DOM element to render into.
   * @param {?function} callback function triggered on completion
   * @return {ReactComponent} Component instance rendered in `container`.
   */
render: function (nextElement, container, callback) {
    return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
  },

參數(shù)說明

nextElement: 這是React.createElement(App, null, React.createElement("div", null, "Hello World")))的結果,babel在解析jsx時,會調用React.createElement將我們寫的組件變成一個 ReactElement

container: ReactDOM.render 的第二個參數(shù),所需要掛載的節(jié)點,document.getElementById('root')

callback: 可選的回調函數(shù),第三個參數(shù)

內部就一句話,關鍵代碼還是ReactMount._renderSubtreeIntoContainer函數(shù)

ReactMount._renderSubtreeIntoContainer

// ReactMount.js

_renderSubtreeIntoContainer: function (parentComponent, nextElement, container, callback) {
    // 校驗 callback
    ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');
    !React.isValidElement(nextElement) ? /**/
    nextElement != null && nextElement.props !== undefined ? /**/

    /**/

    var nextWrappedElement = React.createElement(TopLevelWrapper, {
      child: nextElement
    });
    var nextContext;
    if (parentComponent) {
      var parentInst = ReactInstanceMap.get(parentComponent);
      nextContext = parentInst._processChildContext(parentInst._context);
    } else {
      nextContext = emptyObject;
    }

    var prevComponent = getTopLevelWrapperInContainer(container);
    if (prevComponent) {
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props.child;
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback = callback && function () {
          callback.call(publicInst);
        };
        ReactMount._updateRootComponent(prevComponent, nextWrappedElement, nextContext, container, updatedCallback);
        return publicInst;
      } else {
        ReactMount.unmountComponentAtNode(container);
      }
    }
    var reactRootElement = getReactRootElementInContainer(container);
    var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement); // false
    // 如果DOM元素包含一個由React呈現(xiàn)但不是根元素R的直接子元素,則為True。
    var containerHasNonRootReactChild = hasNonRootReactChild(container); // false

    if (process.env.NODE_ENV !== 'production') {
      /**/
    }

    var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false
    var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
    if (callback) {
      callback.call(component);
    }
    return component;
  }

參數(shù)


流程:
首先檢查callback nextElement是否是合法的,判斷一下類型啥的,然后會使用React.createElement創(chuàng)建一個typeTopLevelWrapperReactElement

var nextWrappedElement = React.createElement(TopLevelWrapper, {
    child: nextElement
});

我們傳入的nextElement會變成nextWrapperElement的一個props;

之后對parentComponent是否存在進行判斷并對nextContext賦值,當前為空賦值為一個空對象emptyObject

調用getTopLevelWrapperInContainer(container)方法,這個方法主要是檢查容器內部是否已經(jīng)存在一個有ReactDOM直接渲染的節(jié)點,當前是無,我們的容器內部是空的

再往下執(zhí)行var reactRootElement = getReactRootElementInContainer(container);

getReactRootElementInContainer

/**
 * @param {DOMElement|DOMDocument} container DOM element that may contain
 * a React component
 * @return {?*} DOM element that may have the reactRoot ID, or null.
 */
function getReactRootElementInContainer(container) {
  if (!container) {
    return null;
  }
  // DOC_NODE_TYPE = 9
  if (container.nodeType === DOC_NODE_TYPE) {
    return container.documentElement;
  } else {
    return container.firstChild;
  }
}

對container.nodeType做判斷,nodeType是html節(jié)點的一個屬性,nodeType = 9 的話表明當前container是document節(jié)點,不是話返回內部的第一個子節(jié)點

接下來執(zhí)行var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);

這個標記變量containerHasReactMarkup 用來判斷當前container是否具有React標記,當前值為 false

下一個var containerHasNonRootReactChild = hasNonRootReactChild(container);如果DOM元素包含一個由React呈現(xiàn)但不是根元素R的直接子元素,則為True。當前為 false

下面根據(jù)以上的幾個變量得出一個標記變量shouldReuseMarkup

var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false

下面就是該函數(shù)的核心了

 var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();
    if (callback) {
      callback.call(component);
    }
    return component;

執(zhí)行ReactMount._renderNewRootComponent()._renderedComponent.getPublicInstance()函數(shù)并將返回值返回出來,如果傳入了callback的話,在return之前在調用一下callback。

那么先看ReactMount._renderNewRootComponent()方法

ReactMount._renderNewRootComponent

傳入的參數(shù)為

nextWrappedElement: nextWrappedElement // 對 TopLevelWrapper調用React.createElement的結果

container: document.getElementById('root')

shouldReuseMarkup: false

nextContext: {}
 _renderNewRootComponent: function (nextElement, container, shouldReuseMarkup, context) {
    process.env.NODE_ENV !== 'production' /**/

    !isValidContainer(container) /**/
    ReactBrowserEventEmitter.ensureScrollValueMonitoring();
    var componentInstance = instantiateReactComponent(nextElement, false);
    // 初始render是同步的,但是在render期間發(fā)生的任何更新,在componentWillMount或componentDidMount中,都將根據(jù)當前的批處理策略進行批處理。
    ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;

    return componentInstance;
  },

流程如下:

對 container 進行驗證

調用ReactBrowserEventEmitter.ensureScrollValueMonitoring() 確保監(jiān)聽瀏覽器滾動,在React15中渲染時應該是不會管頁面中高性能事件的,所以在React16中引入的fiber架構。

調用instantiateReactComponent方法實例化一個ReactComponent,這個方法也是ReactDOM的一個重點,在下篇會說

調用ReactUpdates.batchedUpdates();開始執(zhí)行批量更新,這當中會用到一開始注入的ReactDefaultBatchingStrategy

外部存儲一下當前實例instancesByReactRootID[wrapperID] = componentInstance,對象instancesByReactRootID外部閉包的一個Object,key值為實例的rootID,value值為當前實例化出來的實例

最后return出這個實例。

當前流程圖

本篇留坑

  1. instantiateReactComponent方法

  2. ReactUpdates 文件

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 40、React 什么是React?React 是一個用于構建用戶界面的框架(采用的是MVC模式):集中處理VIE...
    萌妹撒閱讀 1,179評論 0 1
  • 原教程內容詳見精益 React 學習指南,這只是我在學習過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,931評論 1 18
  • 在介紹React渲染機制之間先來說一說下面幾個概念,對于新入手React的學員來說,經(jīng)常會被搞蒙圈。 Rea...
    殊一ONLY閱讀 5,761評論 0 4
  • 原文鏈接地址:https://github.com/Nealyang 轉載請注明出處 前言 戰(zhàn)戰(zhàn)兢兢寫下開篇......
    Nealyang閱讀 2,536評論 0 3
  • 泉州涂嶺鄉(xiāng)的白井村是我的老家,有一年多都沒有回去了,于是我在暑假的時候踏上了“還鄉(xiāng)之路”。 為什么...
    流星小飛豬J閱讀 484評論 0 2

友情鏈接更多精彩內容