Quiet Storm Lyn
  1. 1 Quiet Storm Lyn
  2. 2 The Night We Stood Lyn
  3. 3 Last Surprise Lyn
  4. 4 One Last You Jen Bird
  5. 5 Warcry mpi
  6. 6 Flower Of Life 发热巫女
  7. 7 Time Bomb Veela
  8. 8 Hypocrite Nush
  9. 9 Life Will Change Lyn
  10. 10 かかってこいよ NakamuraEmi
  11. 11 Libertus Chen-U
2018-03-12 21:50:27

React源码解析——组件的挂载

我们知道React应用的常见结构是由JSX语法构成各种各样的组件,然后通过render方法挂载到相关的父组件上构成一颗组件树,最终将整个组件树挂载到真实DOM上。本文分析该过程的实现原理。

组件初始化

既然要挂载组件那必然得先创造一个组件,以下是一个最简单的例子:

import React from 'react';
class A extends React.Component {
    constructor(props) {
        super(props);
          this.state = {};
    }
  render() {
        return <div>hello<div>
  }
}

可以看到我们的组件需要继承自React的Component类,下面就看一下这部分的源码。

var React = {
    // ...
      Component: ReactComponent,
      createElement: createElement
}

function ReactComponent(props, context, updater) {
    this.props = props;
      this.context = context;
      // ...
}

ReactComponent.prototype.setState = function(partialState, callback) {
    // ...
}

ReactComponent.prototype.forceUpdate = function(callback) {
    // ...
}

可以发现我们从React.Component中继承到了React组件的通用属性和方法。那么其结构呢?结构是由JSX提供的,经过babel转义后会发现JSX会转化为React的createElement方法来创建。源码如下:

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

  // Reserved names are extracted
  var props = {};

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

  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;
    // Remaining properties are added to a new props object
    for (propName in config) {
      if (config.hasOwnProperty(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];
    }
    props.children = childArray;
  }

  // Resolve default props
  if (type && type.defaultProps) {
    var defaultProps = type.defaultProps;
    for (propName in defaultProps) {
      if (typeof props[propName] === 'undefined') {
        props[propName] = defaultProps[propName];
      }
    }
  }

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  );
};

代码比较简单,主要是处理参数,将children放入props中。然后通过ReactElement方法创建出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,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

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

  return element;
};

到这里我们就可以发现,每一个组件其实就是一个ReactElement类型的JS对象了,也就是常说的虚拟DOM。

组件的挂载

现在我们已经有了组件对象,就可以通过render方法去挂载了。下面进入到render方法的源码:

var TopLevelWrapper = function () {
  this.rootID = topLevelRootCounter++;
};

TopLevelWrapper.prototype.render = function () {
  // this.props is actually a ReactElement
  return this.props;
};

var ReactMount = {
    // ...
    render: function (nextElement, container, callback) {
        return ReactMount._renderSubtreeIntoContainer(null, nextElement, container, callback);
    },

    //若组件未挂载,那么将react组件挂载到DOM上,若组件已被挂载,那么将执行组件更新机制
    _renderSubtreeIntoContainer:function (parentComponent, nextElement, container, callback) {
        ReactUpdateQueue.validateCallback(callback, 'ReactDOM.render');

        //将组件添加到前一级wrapper的props属性下
        var nextWrappedElement = ReactElement(TopLevelWrapper, null, null, null, null, null, nextElement);
        var nextContext;

        //如果父组件存在,则存储在nextContext中,否则存储空对象
        if (parentComponent) {
            var parentInst = ReactInstanceMap.get(parentComponent);
            nextContext = parentInst._processChildContext(parentInst._context);
        }  else {
            nextContext = emptyObject;
        }

        //判断容器下是否已经存在组件,对于ReactDOM.render()来说为空
        var prevComponent = getTopLevelWrapperInContainer(container);

        //假设该容器已存在组件且类型和索引相同时,依据Diff算法只对当前组件进行更新,否则进行卸载
        if (prevComponent) {
            var prevWrappedElement = prevComponent._currentElement;
            var prevElement = prevWrappedElement.props;
            //组件更新机制在生命周期部分进行解析
            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);
        var containerHasNonRootReactChild = hasNonRootReactChild(container);

        //经过上述流程,确认容器为空或容器内的组件已卸载,那么调用_renderNewRootComponent插入DOM
        var component = ReactMount._renderNewRootComponent(nextWrappedElement, container, shouldReuseMarkup, nextContext)._renderedComponent.getPublicInstance();

        //此处说明ReactDOM.render可以传三个参数,包括回调函数
        if (callback) {
            callback.call(component);
        }

        return component;
}

首先是调用了_renderSubtreeIntoContainer方法,该方法通过ReactElement创建一个虚拟DOM,再调用 _renderNewRootComponent,代码如下:

_renderNewRootComponent: function(
    nextElement,
    container,
    shouldReuseMarkup,
    context
  ) {
    var componentInstance = instantiateReactComponent(nextElement, null);

    ....

    ReactUpdates.batchedUpdates(
      batchedMountComponentIntoNode,
      componentInstance,
      reactRootID,
      container,
      shouldReuseMarkup,
      context
    );

    ....

    return componentInstance;
  }

首先通过instantiateReactComponent方法创建虚拟DOM对应的ReactComponent对象,根据nextElement参数的不同会创建四大类组件:

  • 为空时创建ReactEmptyComponent组件
  • 为React组件时创建ReactCompositeComponent组件
  • 为字符串或数字时创建ReactTextComponent组件
  • 为DOM时创建ReactDOMComponent组件

生命周期相关的方法就在这些组件中初始化。

再通过batchedUpdates方法去调用batchedMountComponentIntoNode,该方法又以事务的形式去调用mountComponentIntoNode,源码如下:

function mountComponentIntoNode(wrapperInstance, container, transaction, shouldReuseMarkup, context) {
  var markerName;
  if (ReactFeatureFlags.logTopLevelRenders) {
    var wrappedElement = wrapperInstance._currentElement.props;
    var type = wrappedElement.type;
    markerName = 'React mount: ' + (typeof type === 'string' ? type : type.displayName || type.name);
    console.time(markerName);
  }

  // 调用对应ReactComponent中的mountComponent方法来渲染组件
  // mountComponent返回React组件解析的HTML。不同的ReactComponent的mountComponent策略不同,可以看做多态
  // 上面的<h1>Hello, world!</h1>, 对应的是ReactDOMTextComponent,最终解析成的HTML为
  // <h1 data-reactroot="">Hello, world!</h1>
  var markup = ReactReconciler.mountComponent(wrapperInstance, transaction, null, ReactDOMContainerInfo(wrapperInstance, container), context);

  if (markerName) {
    console.timeEnd(markerName);
  }

  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
  // 将解析出来的HTML插入DOM中
  ReactMount._mountImageIntoNode(markup, container, wrapperInstance, shouldReuseMarkup, transaction);
}

这一步会根据以上四类组件的不同分别调用其mountComponent方法去渲染组件为HTML,最后通过_mountImageIntoNode将这个HTML设置到container这个DOM元素的innerHTML上就完成了组件的挂载。

总结

组件的挂载流程按关键节点大致可分为如下几步:

  1. 通过React.createElement()创建ReactElement对象,即虚拟DOM
  2. 通过instantiateReactComponent()根据虚拟DOM的类型分别创建不同类型的ReactComponent对象
  3. 调用不同类型对象的mountComponent()得到HTML
  4. _mountImageIntoNode()将HTML通过innerHTML插入到真实DOM中

-- EOF --

添加在分类「 前端开发 」下,并被添加 「React」 标签。