阅读 45

React@15.6.2源码解析---从 ReactDOM.render 到页面渲染(1)ReactMount

之前介绍了React16.8版本的React公用API,本着学习最新版的React的想法,但是败在了Fiber的阵下,还有回过头来写搞明白React15的源码,毕竟从15到16是一次重大的更新。本文中React源码版本为 15.6.2 ,望各位看官找准版本号,不同的版本还是有着细微的区别的

值得一提的是,在阅读源码时,在Chrome中打断点是一个很好的操作,可以了解到函数的调用栈,变量的值,一步一步的调试还可以了解整个执行的流程,一边调试一边记录着流程一边在加以理解一边感慨这神乎其技的封装。

博客会同步到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'));
复制代码

一个立即执行函数,返回一个名为App的构造函数,内部的componentWillMount render等方法,等会通过Object.defineProperty方法添加到App的原型链中。之后使用React.createElementApp转换为ReactElement对象传入到ReactDOM.render

看源码需要扎实的Js基础,原型链、闭包、this指向、模块化、Object.defineProperty等常用的方法都是必须提前掌握的。

ReactDOM.render

在引入ReactDOM.js文件的时候,从上往下仔细看会发现有这么一行代码是在引入的时候被执行了ReactDefaultInjection.inject();,这个ReactDefaultInjection调用了其内部的一个inject方法,主要目的是进行一次全局的依赖注入,本博主一开始光注意着研究ReactDOM.render了,漏了这一句,导致后面有的东西很迷,所以在这提个醒,在引入一个文件时,文件内部有的函数是没有被导出的反而是在引入文件时直接执行的。这个inject具体的代码后面用到时会进行详细的介绍。

下面就是ReactDOM文件的代码了

/* 各种文件的引入 */

// 执行依赖注入
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,一旦找关键词会错过很多细节。一旦错过了那么导致的结局就是卧槽,这个东西什么时候被赋值了,卧槽,这个属性哪里来的尴尬局面。所以再一次强调,打断点的好处。Chrome断点,🧱

那么你会发现,有这么一个构造函数

/**
 * 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时,传递的参数被这个构造函数给包裹起来。

// 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);
  },
复制代码

参数说明

nextElement: 这是React.createElement(App, null, React.createElement("div", null, "Hello World")))的结果,babel在解析jsx时,会调用React.createElement将我们写的组件变成一个 ReactElement

container: ReactDOM.render 的第二个参数,所需要挂载的节点,document.getElementById('root')

callback: 可选的回调函数,第三个参数

内部就一句话,关键代码还是ReactMount._renderSubtreeIntoContainer函数

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呈现但不是根元素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;
  }
复制代码

参数

image_1dlk722i093bbvvosn8gm1h6pp.png-74.5kB

流程: 首先检查callback nextElement是否是合法的,判断一下类型啥的,然后会使用React.createElement创建一个typeTopLevelWrapperReactElement

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

我们传入的nextElement会变成nextWrapperElement的一个props;

image_1dlk7bg5l1vbf1d1dhbteq8f0d16.png-50.2kB

之后对parentComponent是否存在进行判断并对nextContext赋值,当前为空赋值为一个空对象emptyObject

调用getTopLevelWrapperInContainer(container)方法,这个方法主要是检查容器内部是否已经存在一个有ReactDOM直接渲染的节点,当前是无,我们的容器内部是空的

再往下执行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节点的一个属性,nodeType = 9 的话表明当前container是document节点,不是话返回内部的第一个子节点

接下来执行var containerHasReactMarkup = reactRootElement && !!internalGetID(reactRootElement);

这个标记变量containerHasReactMarkup 用来判断当前container是否具有React标记,当前值为 false

下一个var containerHasNonRootReactChild = hasNonRootReactChild(container);如果DOM元素包含一个由React呈现但不是根元素R的直接子元素,则为True。当前为 false

下面根据以上的几个变量得出一个标记变量shouldReuseMarkup

var shouldReuseMarkup = containerHasReactMarkup && !prevComponent && !containerHasNonRootReactChild; // false
复制代码

下面就是该函数的核心了

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

执行ReactMount._renderNewRootComponent()._renderedComponent.getPublicInstance()函数并将返回值返回出来,如果传入了callback的话,在return之前在调用一下callback。

那么先看ReactMount._renderNewRootComponent()方法

ReactMount._renderNewRootComponent

传入的参数为

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期间发生的任何更新,在componentWillMount或componentDidMount中,都将根据当前的批处理策略进行批处理。
    ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, componentInstance, container, shouldReuseMarkup, context);
    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;

    return componentInstance;
  },
复制代码

流程如下:

对 container 进行验证

调用ReactBrowserEventEmitter.ensureScrollValueMonitoring() 确保监听浏览器滚动,在React15中渲染时应该是不会管页面中高性能事件的,所以在React16中引入的fiber架构。

调用instantiateReactComponent方法实例化一个ReactComponent,这个方法也是ReactDOM的一个重点,在下篇会说

调用ReactUpdates.batchedUpdates();开始执行批量更新,这当中会用到一开始注入的ReactDefaultBatchingStrategy

外部存储一下当前实例instancesByReactRootID[wrapperID] = componentInstance,对象instancesByReactRootID外部闭包的一个Object,key值为实例的rootID,value值为当前实例化出来的实例

最后return出这个实例。

当前流程图

image_1dllogis11e54igc14j21hjq4ov1j.png-58.4kB

本篇留坑

  1. instantiateReactComponent方法

  2. ReactUpdates 文件