zl程序教程

您现在的位置是:首页 >  Java

当前栏目

React源码学习入门(七)详解ReactMount入口

2023-02-18 16:33:54 时间

详解ReactMount入口

本文基于React v15.6.2版本介绍,原因请参见新手如何学习React源码

源码分析

ReactMount的源码位于src/renderers/dom/client/ReactMount.js

ReactMount中,我们常用的API是renderunmountComponentAtNode,而render则是整个应用的启动入口:

ReactDOM.render(
  <Counter />,
  document.getElementById('container')
);

render的首次初始化核心方法实现在_renderNewRootComponent中:

  _renderNewRootComponent: function(
    nextElement,
    container,
    shouldReuseMarkup,
    context,
  ) {
    
    ReactBrowserEventEmitter.ensureScrollValueMonitoring();
    var componentInstance = instantiateReactComponent(nextElement, false);

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

    var wrapperID = componentInstance._instance.rootID;
    instancesByReactRootID[wrapperID] = componentInstance;

    return componentInstance;
  },

这个方法核心做了几件事情:

  1. 实例化nextElement,创建对应的辅助类
  2. 调用batchedUpdates
  3. 设置_instancesByReactRootID,这个被devtools使用,不重要

前两步其实都非常核心,我们展开讲解一下。

首先,实例化的这个nextElement是什么?实际上在调用链的上一层函数_renderSubtreeIntoContainer可以找到:

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

实际上是React自己调用createElement创建的一个Element,而它的props的child是nextElement,这个element就是我们调用render的时候传入的根组件了。

那么TopLevelWrapper是什么呢?在ReactMount中是这么定义的:

var topLevelRootCounter = 1;
var TopLevelWrapper = function() {
  this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.isReactComponent = {};
if (__DEV__) {
  TopLevelWrapper.displayName = 'TopLevelWrapper';
}
TopLevelWrapper.prototype.render = function() {
  return this.props.child;
};
TopLevelWrapper.isReactTopLevelWrapper = true;

实际上就是一个ReactComponent,而它挂了一个成员rootID,就是一层容器,React给我们传入的Element又包裹了一层Wrapper组件,主要目的还是为了统一创建一个ReactCompositeComponent的控制类,上篇文章讲了React有四种控制类,而我们自己传入的根组件并不能确认是属于哪种类型,于是React为了更好地处理多次调用render的更新逻辑,就统一包了一个WrapperComponnent

所以这里在_renderNewRootComponent时,调用instantiateReactComponent创建出来的实际上是Wrapper的instance,我们观察一下被传入batchedUpdate的几个参数:

  • componentInstance,是Wrapper的控制类,是一个ReactCompositeComponent
  • container,是我们的DOM容器
  • shouldReuseMarkup,与二次render有关,先忽略
  • context,与parent的context有关,这里属于非根节点render的场景,先忽略

在Mount过程中调用batchedUpdate,其实是为了在rending的生命周期中,例如componentWillMount或者componentDidMount,去调用setState更新能够做到批量更新一次,这个细节我们在后面的文章里面再细说。

接着我们看一下batchedMountComponentIntoNode

function batchedMountComponentIntoNode(
  componentInstance,
  container,
  shouldReuseMarkup,
  context,
) {
  var transaction = ReactUpdates.ReactReconcileTransaction.getPooled(
    /* useCreateElement */
    !shouldReuseMarkup && ReactDOMFeatureFlags.useCreateElement,
  );
  transaction.perform(
    mountComponentIntoNode,
    null,
    componentInstance,
    container,
    transaction,
    shouldReuseMarkup,
    context,
  );
  ReactUpdates.ReactReconcileTransaction.release(transaction);
}

这个方法核心是开启了ReactReconcileTransaction,这个transaction将会伴随mount的整个周期,这里采用之前讲过的对象池来做复用,不再赘述。

接下来看一下这个transaction是什么,源码位于src/renderers/dom/client/ReactReconcileTransaction.js

function ReactReconcileTransaction(useCreateElement: boolean) {
  this.reinitializeTransaction();
  this.renderToStaticMarkup = false;
  this.reactMountReady = CallbackQueue.getPooled(null);
  this.useCreateElement = useCreateElement;
}

var Mixin = {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },

  getReactMountReady: function() {
    return this.reactMountReady;
  },

  getUpdateQueue: function() {
    return ReactUpdateQueue;
  },

  checkpoint: function() {
    return this.reactMountReady.checkpoint();
  },

  rollback: function(checkpoint) {
    this.reactMountReady.rollback(checkpoint);
  },

  destructor: function() {
    CallbackQueue.release(this.reactMountReady);
    this.reactMountReady = null;
  },
};

可以看到这个transaction上挂载了几个属性和方法,需要关注的是reactMountReady是一个队列,这个在挂载过程中会存放componentDidMount的回调,挂载完成后依次触发,所以说componentDidMount整体其实是异步的。

接下来再看一下wrapper的部分:

var SELECTION_RESTORATION = {
  initialize: ReactInputSelection.getSelectionInformation,
  close: ReactInputSelection.restoreSelection,
};

var EVENT_SUPPRESSION = {
  initialize: function() {
    var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
    ReactBrowserEventEmitter.setEnabled(false);
    return currentlyEnabled;
  },
  close: function(previouslyEnabled) {
    ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
  },
};

var ON_DOM_READY_QUEUEING = {
  initialize: function() {
    this.reactMountReady.reset();
  },

  close: function() {
    this.reactMountReady.notifyAll();
  },
};

var TRANSACTION_WRAPPERS = [
  SELECTION_RESTORATION,
  EVENT_SUPPRESSION,
  ON_DOM_READY_QUEUEING,
];

前两个和保存事件状态有关,而第三个Wrapper则是处理队列的通知,保证执行完Mount之后回调能够正常触发。

最后我们看一下mountComponentIntoNode的实现,这个方法也是整个Mount流程的最后一步:

function mountComponentIntoNode(
  wrapperInstance,
  container,
  transaction,
  shouldReuseMarkup,
  context,
) {
  var markup = ReactReconciler.mountComponent(
    wrapperInstance,
    transaction,
    null,
    ReactDOMContainerInfo(wrapperInstance, container),
    context,
    0 /* parentDebugID */,
  );

  wrapperInstance._renderedComponent._topLevelWrapper = wrapperInstance;
  ReactMount._mountImageIntoNode(
    markup,
    container,
    wrapperInstance,
    shouldReuseMarkup,
    transaction,
  );
}

这里核心做了两件事:

  1. 调用ReactReconciler.mountComponent拿到markup,这个是个递归的过程,也是stack核心,限于篇幅我们在后续文章中详解,可以认为最终拿到的markup是一个已经处理好的DOM节点(开启createElement的新版本),或是要插入的HTML片段(老版本)。
  2. 调用ReactMount._mountImageIntoNode去挂载到真实的DOM容器下。

这里额外注意的一点是新增加了一个参数containerInfo,我们看一下ReactDOMContainerInfo,源码位于src/renderers/dom/shared/ReactDOMContainerInfo.js

function ReactDOMContainerInfo(topLevelWrapper, node) {
  var info = {
    _topLevelWrapper: topLevelWrapper,
    _idCounter: 1,
    _ownerDocument: node
      ? node.nodeType === DOC_NODE_TYPE ? node : node.ownerDocument
      : null,
    _node: node,
    _tag: node ? node.nodeName.toLowerCase() : null,
    _namespaceURI: node ? node.namespaceURI : null,
  };
  if (__DEV__) {
    info._ancestorInfo = node
      ? validateDOMNesting.updatedAncestorInfo(null, info._tag, null)
      : null;
  }
  return info;
}

生成的containerInfo主要挂载了topLevelWrapper的实例,container本身的节点信息,和一个idCounter,这些信息在后续更新过程中十分有用,可以稍微记住这里,后续更新再回过头来看。

最后再稍微看下_mountImageIntoNode,实际上在首次挂载时它的执行逻辑非常简单:

  _mountImageIntoNode: function(
    markup,
    container,
    instance,
    shouldReuseMarkup,
    transaction,
  ) {
    if (transaction.useCreateElement) {
      while (container.lastChild) {
        container.removeChild(container.lastChild);
      }
      DOMLazyTree.insertTreeBefore(container, markup, null);
    } else {
      setInnerHTML(container, markup);
      ReactDOMComponentTree.precacheNode(instance, container.firstChild);
    }
  },
};

可以看到主要是调用DOMLazyTree.insertTreeBefore去插入节点,DOMLazyTree存在的意义是为了让IE系列有更好的性能,React团队调研发现直接插入大量节点在IE/Edge下不如小批量插入节点来得快(相差10倍以上的性能),因此这个文件专门用来磨平差异,我们这里可以简单理解为它就是原生的insertBefore方法。

小结一下

上述过程中讲了非常多ReactMount的细节,实际上很多都是为了Update去做准备,我们抛开Update不谈,只看Mount,实际上非常简单,整个Mount它就做了三件事情:

  1. 创建了一个根节点(在内部是统一包裹了Wrapper组件)实例。
  2. 调用ReactReconciler.mountComponent获取Markup,这个是个递归的过程。
  3. 调用mountImageIntoNode将Markup挂载到容器的DOM节点上。