ReactDOM 是如何把组件渲染到 DOM 中的?

·  阅读 1267
ReactDOM 是如何把组件渲染到 DOM 中的?
原文链接: www.kisnows.com

当我们在开发 React 项目中,第一次调用 ReactDOM.render 的时候都发生了什么呢?
今天就从源码角度来追踪一下这个问题(主要看流程, 而不纠结与细节)。

ReactDOMStackEntry

首先我们可以从 ReactDOM 的入口文件 ReactDOMStackEntry.js 中找到 render 方法。可以看到,render 方法是 ReactMount 组件提供的。

var ReactDOMStack = {
  findDOMNode: findDOMNode,
  render: ReactMount.render,
  unmountComponentAtNode: ReactMount.unmountComponentAtNode,
  version: ReactVersion,
  /* eslint-disable camelcase */
  unstable_batchedUpdates: ReactGenericBatching.batchedUpdates,
  unstable_renderSubtreeIntoContainer: ReactMount.renderSubtreeIntoContainer,
  /* eslint-enable camelcase */
  __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
    // For TapEventPlugin which is popular in open source
    EventPluginHub: require('EventPluginHub'),
    // Used by test-utils
    EventPluginRegistry: require('EventPluginRegistry'),
    EventPropagators: require('EventPropagators'),
    ReactControlledComponent: require('ReactControlledComponent'),
    ReactDOMComponentTree,
    ReactDOMEventListener: require('ReactDOMEventListener'),
    ReactUpdates: ReactUpdates,
  },
};复制代码

ReactMount.render

render: function(nextElement, container, callback) {
  return ReactMount._renderSubtreeIntoContainer(
    null,
    nextElement,
    container,
    callback,
  );
},复制代码

又调到了 _renderSubtreeIntoContainer 方法, 这个方法核心内容如下:

ReactMount._renderSubtreeIntoContainer

_renderSubtreeIntoContainer: function(
    parentComponent,
    nextElement,
    container,
    callback,
  ) {
    callback = callback === undefined ? null : callback;
    if (!React.isValidElement(nextElement)) {
      ...
    }
    // 创建下一个 wrapped 元素
    var nextWrappedElement = React.createElement(TopLevelWrapper, {
      child: nextElement,
    });
    var nextContext = getContextForSubtree(parentComponent);
    // 拿到当前的顶层容器组件
    var prevComponent = getTopLevelWrapperInContainer(container);
    // 对于第一次 render 来说,prevComponent 为 null
    if (prevComponent) {
      var prevWrappedElement = prevComponent._currentElement;
      var prevElement = prevWrappedElement.props.child;
      if (shouldUpdateReactComponent(prevElement, nextElement)) {
        var publicInst = prevComponent._renderedComponent.getPublicInstance();
        var updatedCallback =
          callback &&
          function() {
            validateCallback(callback);
            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);
    var shouldReuseMarkup =
      containerHasReactMarkup &&
      !prevComponent &&
      !containerHasNonRootReactChild;
    var component = ReactMount._renderNewRootComponent(
      nextWrappedElement,
      container,
      shouldReuseMarkup,
      nextContext,
      callback,
    )._renderedComponent.getPublicInstance();
    return component;
  },复制代码

这里有几个方法,分别是:

  • getTopLevelWrapperInContainer
  • shouldUpdateReactComponent
  • _renderNewRootComponent

getTopLevelWrapperInContainer

这个方法用来拿到现有的顶层容器组件,相关代码如下。

function getTopLevelWrapperInContainer(container) {
  var root = getHostRootInstanceInContainer(container);
  return root ? root._hostContainerInfo._topLevelWrapper : null;
}
function getHostRootInstanceInContainer(container) {
  var rootEl = getReactRootElementInContainer(container);
  var prevHostInstance =
    rootEl && ReactDOMComponentTree.getInstanceFromNode(rootEl);
  return prevHostInstance && !prevHostInstance._hostParent
    ? prevHostInstance
    : null;
}复制代码

调用了两个方法,getReactRootElementInContainerReactDOMComponentTree.getInstanceFromNode.

getReactRootElementInContainer

function getReactRootElementInContainer(container) {
  if (!container) {
    return null;
  }
  if (container.nodeType === DOCUMENT_NODE) {
    return container.documentElement;
  } else {
    return container.firstChild;
  }
}复制代码

ReactDOMComponentTree

这个模块有三个方法,分别是:

  • precacheChildNodes 在 DOM 节点上存储相应的 React 实例
  • getNodeFromInstance 从一个实例上获取到对应的 DOM 节点
  • getInstanceFromNode 从一个 DOM 节点上获取到对应的实例

shouldUpdateReactComponent

判断是否需要更新组件。

function shouldUpdateReactComponent(prevElement, nextElement) {
  var prevEmpty = prevElement === null || prevElement === false;
  var nextEmpty = nextElement === null || nextElement === false;
  if (prevEmpty || nextEmpty) {
    return prevEmpty === nextEmpty;
  }
  var prevType = typeof prevElement;
  var nextType = typeof nextElement;
  if (prevType === 'string' || prevType === 'number') {
    return nextType === 'string' || nextType === 'number';
  } else {
    return (
      nextType === 'object' &&
      prevElement.type === nextElement.type &&
      prevElement.key === nextElement.key
    );
  }
}复制代码

可以看到逻辑是这样的:

  • 前后两次元素都为 null 返回 true
  • 如果是 textComponent,那么直接更新
  • 否则当为 DOM 元素或者 React 元素时,且 type 和 key 都相同时返回 true, 执行 update

ReactMount._renderNewRootComponent

这个方法是 _renderSubtreeIntoContainer 的核心,用来把一个新的组件挂载到 DOM 中。

_renderNewRootComponent: function(
  nextElement,
  container,
  shouldReuseMarkup,
  context,
  callback,
) {
  // 通过 instantiateReactComponent 拿到 React Component 组件实例
  var componentInstance = instantiateReactComponent(nextElement, false);
  if (callback) {
    componentInstance._pendingCallbacks = [
      function() {
        validateCallback(callback);
        callback.call(
          componentInstance._renderedComponent.getPublicInstance(),
        );
      },
    ];
  }
  // The initial render is synchronous but any updates that happen during
  // rendering, in componentWillMount or componentDidMount, will be batched
  // according to the current batching strategy.
  ReactUpdates.batchedUpdates(
    batchedMountComponentIntoNode,
    componentInstance,
    container,
    shouldReuseMarkup,
    context,
  );
  var wrapperID = componentInstance._instance.rootID;
  instancesByReactRootID[wrapperID] = componentInstance;
  return componentInstance;
},复制代码

instantiateReactComponent

根据传入的参数来生成不同的 React Component, 核心代码:

if (node === null || node === false) {
  instance = ReactEmptyComponent.create(instantiateReactComponent);
} else if (typeof node === 'object') {
  var element = node;
  var type = element.type;
  if (typeof type !== 'function' && typeof type !== 'string') {
    ...
  }
  // Special case string values
  if (typeof element.type === 'string') {
    instance = ReactHostComponent.createInternalComponent(element);
  } else if (isInternalComponentType(element.type)) {
    // This is temporarily available for custom components that are not string
    // representations. I.e. ART. Once those are updated to use the string
    // representation, we can drop this code path.
    instance = new element.type(element);
    // We renamed this. Allow the old name for compat. :(
    if (!instance.getHostNode) {
      instance.getHostNode = instance.getNativeNode;
    }
  } else {
    instance = new ReactCompositeComponentWrapper(element);
  }
} else if (typeof node === 'string' || typeof node === 'number') {
  instance = ReactHostComponent.createInstanceForText(node);
} else {
  invariant(false, 'Encountered invalid React node of type %s', typeof node);
}复制代码

从代码中可以看到,根据 element.type 的不同,有三个方法来生成三种不同 React 组件实例,分别为:

  • ReactHostComponent.createInternalComponent(element)
  • new ReactCompositeComponentWrapper(element)
  • ReactHostComponent.createInstanceForText(node);

可能读者会注意到,当 isInternalComponentType(element.type) 成立时, instance = new element.type(element); 的这段代码被我忽略了,那是因为这个是 React 封装的内部组件不是由字符串表达时的解决方法,我们是不用关心的。
我们来看上面的三个方法,其中两个方法都调用了 ReactHostComponent 模块。

ReactHostComponent

核心代码:

var ReactHostComponentInjection = {
  // This accepts a class that receives the tag string. This is a catch all
  // that can render any kind of tag.
  injectGenericComponentClass: function(componentClass) {
    genericComponentClass = componentClass;
  },
  // This accepts a text component class that takes the text string to be
  // rendered as props.
  injectTextComponentClass: function(componentClass) {
    textComponentClass = componentClass;
  },
};
function createInternalComponent(element) {
  invariant(
    genericComponentClass,
    'There is no registered component for the tag %s',
    element.type,
  );
  return new genericComponentClass(element);
}
/**
 * @param {ReactText} text
 * @return {ReactComponent}
 */
function createInstanceForText(text) {
  return new textComponentClass(text);
}复制代码

就是提供了两个方法来创建组件,而其中两个组件 class 的实现是通过其他模块注入进来的,那到底是从哪里注入进来的呢。
经过一番查找,发现是在 ReactDOMStackInjection.js 中注入的,我们看一下代码:

var ReactComponentEnvironment = require('ReactComponentEnvironment');
var ReactComponentBrowserEnvironment = require('ReactComponentBrowserEnvironment');
var ReactDOMComponent = require('ReactDOMComponent');
var ReactDOMComponentTree = require('ReactDOMComponentTree');
var ReactDOMEmptyComponent = require('ReactDOMEmptyComponent');
var ReactDOMTextComponent = require('ReactDOMTextComponent');
var ReactDefaultBatchingStrategy = require('ReactDefaultBatchingStrategy');
var ReactEmptyComponent = require('ReactEmptyComponent');
var ReactGenericBatching = require('ReactGenericBatching');
var ReactHostComponent = require('ReactHostComponent');
var ReactReconcileTransaction = require('ReactReconcileTransaction');
var ReactUpdates = require('ReactUpdates');
var findDOMNode = require('findDOMNode');
var getHostComponentFromComposite = require('getHostComponentFromComposite');
ReactGenericBatching.injection.injectStackBatchedUpdates(
  ReactUpdates.batchedUpdates,
);
ReactHostComponent.injection.injectGenericComponentClass(ReactDOMComponent);
ReactHostComponent.injection.injectTextComponentClass(ReactDOMTextComponent);
ReactEmptyComponent.injection.injectEmptyComponentFactory(function(
  instantiate,
) {
  return new ReactDOMEmptyComponent(instantiate);
});
ReactUpdates.injection.injectReconcileTransaction(ReactReconcileTransaction);
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);
ReactComponentEnvironment.injection.injectEnvironment(
  ReactComponentBrowserEnvironment,
);
findDOMNode._injectStack(function(inst) {
  inst = getHostComponentFromComposite(inst);
  return inst ? ReactDOMComponentTree.getNodeFromInstance(inst) : null;
});复制代码

可以看到,这个模块通过 ReactHostComponent.injection 注入了 ReactDOMComponent 和 ReactDOMTextComponent. 同时也注入了一些其他模块,这个我们后面还会用到。

这里 ReactDOMComponent 和 ReactDOMTextComponent 才是真是的生成 DOM 标记的模块,它们的内容过多,但是代码比较简单了,这里就不细说了。总之调用这两个模块的 mountComponent 方法都会生成 DOM Markup. 不同的地方在于, ReactDOMComponent 会如下结构的 Markup

{
  node: node,
  children: [],
  html: null,
  text: null,
  toString
}复制代码

而 ReactDOMTextComponent 会直接生成要渲染在 DOM 里面的 String 类型的文本。

ReactCompositeComponent

最后一类组件应该是 ReactCompositeComponentWrapper, 但查看 instantiateReactComponent 里面的这两段代码:

var ReactCompositeComponentWrapper = function(element) {
  this.construct(element);
};
Object.assign(
  ReactCompositeComponentWrapper.prototype,
  ReactCompositeComponent,
  {
    _instantiateReactComponent: instantiateReactComponent,
  },
);复制代码

当调用 this.construct 的时候,还是调用到了 ReactCompositeComponent ,这个就是用户自定义的组件。

ReactUpdates.batchedUpdates

function batchedUpdates(callback, a, b, c, d, e) {
  ensureInjected();
  return batchingStrategy.batchedUpdates(callback, a, b, c, d, e);
}复制代码

可以看到 ReactUpdates 里面是 通过 batchingStrategy 调用 batchedUpdate. 而 batchingStrategy 也是前面说到的 ReactDOMStackInjection 来注入进去的。

ReactUpdates.injection.injectReconcileTransaction(ReactReconcileTransaction);
ReactUpdates.injection.injectBatchingStrategy(ReactDefaultBatchingStrategy);复制代码

ReactUpdates 共注入了两个模块,分别是 ReactReconcileTransaction 和 ReactDefaultBatchingStrategy.

先来看一下 ReactDefaultBatchingStrategy, ReactReconcileTransaction 后面碰到再说,来看一下它的代码:

var ReactUpdates = require('ReactUpdates');
var Transaction = require('Transaction');
var emptyFunction = require('fbjs/lib/emptyFunction');
var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}
Object.assign(ReactDefaultBatchingStrategyTransaction.prototype, Transaction, {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
});
var transaction = new ReactDefaultBatchingStrategyTransaction();
var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,
  /**
   * Call the provided function in a context within which calls to `setState`
   * and friends are batched such that components aren't updated unnecessarily.
   */
  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      // 如果当前 updates 已经完成,那么直接调用 callback
      return callback(a, b, c, d, e);
    } else {
      // 通过事务的方式去调用 callback
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },
};复制代码

可以看到我们前面调用的 ReactUpdates.batchUpdates 实际上调用到了这里的 batchedUpdates, 里面的逻辑也很简单。

这里面用到了一个 transaction 方法,这里我理解为“事务”。也就是说当如果当前正在进行一次更新,那么就通过事务的方式去调用这个 callback.

transaction

transaction 在 React 源码里面使用非常广泛,作用是通过事务的方式去调用一个方法。

用一个或多个 wrapper 把方法包裹起来,在方法调用前和调用之后依次执行。事务会确保 wrapper 的 initialize 和 close 方法都会执行,无论要执行的方法执行成功或失败报错,看看它的源码解释就很明白了:

* <pre>
*                       wrappers (injected at creation time)
*                                      +        +
*                                      |        |
*                    +-----------------|--------|--------------+
*                    |                 v        |              |
*                    |      +---------------+   |              |
*                    |   +--|    wrapper1   |---|----+         |
*                    |   |  +---------------+   v    |         |
*                    |   |          +-------------+  |         |
*                    |   |     +----|   wrapper2  |--------+   |
*                    |   |     |    +-------------+  |     |   |
*                    |   |     |                     |     |   |
*                    |   v     v                     v     v   | wrapper
*                    | +---+ +---+   +---------+   +---+ +---+ | invariants
* perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | |   | |   |   |         |   |   | |   | |
*                    | +---+ +---+   +---------+   +---+ +---+ |
*                    |  initialize                    close    |
*                    +-----------------------------------------+
* </pre>复制代码

这里我们再回到刚才的 ReactDefaultBatchingStrategy, 代码里面有两个 wrapper.

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  },
};
var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates),
};复制代码

它们的 initialize 方法都是一个空函数,close 方法分别是:

  1. 把当前的更新状态置为 false
  2. flushBatchedUpdates 这个方法比较复杂,这里不展开讲,主要是确保所有组件能够正确更新(flushBatchedUpdates->ReactUpdates.runBatchedUpdates->ReactCompositeComponent.performUpdateIfNecessary

现在回到上面的代码:

ReactUpdates.batchedUpdates(
  batchedMountComponentIntoNode,
  componentInstance,
  container,
  shouldReuseMarkup,
  context,
);复制代码

可以知道,这里是调用了 batchedMountComponentIntoNode 进行后续的工作。

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

这里第一条赋值语句就用到了上面注入到 ReactUpdates 里面的另一个模块 ReactReconcileTransaction 即 React 的调度事务模块。

var SELECTION_RESTORATION = {
  initialize: ReactInputSelection.getSelectionInformation,
  close: ReactInputSelection.restoreSelection,
};
/**
 * Suppresses events (blur/focus) that could be inadvertently dispatched due to
 * high level DOM manipulations (like temporarily removing a text input from the
 * DOM).
 */
var EVENT_SUPPRESSION = {
  initialize: function() {
    var currentlyEnabled = ReactBrowserEventEmitter.isEnabled();
    ReactBrowserEventEmitter.setEnabled(false);
    return currentlyEnabled;
  },
  close: function(previouslyEnabled) {
    ReactBrowserEventEmitter.setEnabled(previouslyEnabled);
  },
};
/**
 * Provides a queue for collecting `componentDidMount` and
 * `componentDidUpdate` callbacks during the transaction.
 */
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,
];
function ReactReconcileTransaction(useCreateElement) {
  this.reinitializeTransaction();
  this.renderToStaticMarkup = false;
  this.reactMountReady = CallbackQueue.getPooled();
  this.useCreateElement = useCreateElement;
}
...
var Mixin = {
  getTransactionWrappers: function() {
    return TRANSACTION_WRAPPERS;
  },
  ...
}
Object.assign(ReactReconcileTransaction.prototype, Transaction, Mixin);
PooledClass.addPoolingTo(ReactReconcileTransaction);复制代码

这里也是通过事务的方式去调用,它有三个 wrapper:

  • Selection Restoration 在更新过程中尽可能不打扰用户的选中范围(selection range)
  • Event Suppression 抑制一些不需要的事件分发,比如暂时性删除一个 input 元素而导致的 blur 事件
  • On DOM Ready Queueing 在事务执行过程中,提供一个 componentDidMountcomponentDidUpdate 回调函数的队列

而后面的 getPooled 方法,是一个利用实例池来避免不必要的 GC 的方法,不过多解释。

接着用这个事务的方式去调用 mountComponentIntoNode, 详细的看一下这个方法。

mountComponentIntoNode

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

这里看到了重点变量 markup 的操作, 这个就是我们最终要往 DOM 里面渲染的对象。通过 ReactReconciler.mountComponent 方法来得到 markup. ReactReconciler.mountComponent 的源码如下:

mountComponent: function(
  internalInstance,
  transaction,
  hostParent,
  hostContainerInfo,
  context,
  parentDebugID, // 0 in production and for roots
) {
  var markup = internalInstance.mountComponent(
    transaction,
    hostParent,
    hostContainerInfo,
    context,
    parentDebugID,
  );
  if (
    internalInstance._currentElement &&
    internalInstance._currentElement.ref != null
  ) {
    transaction.getReactMountReady().enqueue(attachRefs, internalInstance);
  }
  return markup;
},复制代码

它又调用了 internalInstance.mountComponent, 这里的 internalInstance 其实就是前面说的通过 instantiateReactComponent 得到的 React Component 实例。

// _renderNewRootComponent
var componentInstance = instantiateReactComponent(nextElement, false)复制代码

这里的 nextElement 是要渲染的 React root 元素。

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

TopLevelWrapper 的实现,需要注意它的 render 方法。

var TopLevelWrapper = function() {
  this.rootID = topLevelRootCounter++;
};
TopLevelWrapper.prototype.isReactComponent = {};
TopLevelWrapper.prototype.render = function() {
  return this.props.child;
};
TopLevelWrapper.isReactTopLevelWrapper = true;复制代码

它的 render 方法里面返回的 this.props.child 就是 nextElement 也就是我们项目代码里面入口 ReactDOM.render(<App/>,document.getElementById('root')) 里面的 <App/>.

回到 ReactReconciler.mountComponent 里面的 internalInstance.mountComponent. 通过前面的讲到的 instantiateReactComponent 我们知道返回的组件有三类:

  1. ReactDOMComponent
  2. ReactDOMTextComponent
  3. ReactCompositeComponent

前两类很简单,都是 DOM 本身的元素,最终会渲染出来它们对应的 Markup. 而 ReactCompositeComponent 比较复杂,我们只看关键代码:

//ReactCompositeComponent.mountComponent
    var Component = this._currentElement.type;
    var updateQueue = transaction.getUpdateQueue();
    // Initialize the public class
    var doConstruct = shouldConstruct(Component);
    var inst = this._constructComponent(
      doConstruct,
      publicProps,
      publicContext,
      updateQueue,
    );
    var renderedElement;
    if (!doConstruct && (inst == null || inst.render == null)) {
      renderedElement = inst;
      inst = new StatelessComponent(Component);
      this._compositeType = ReactCompositeComponentTypes.StatelessFunctional;
    } else {
     。...
    }
      markup = this.performInitialMount(
        renderedElement,
        hostParent,
        hostContainerInfo,
        transaction,
        context,
      );复制代码

首先得到 inst , 得到 inst 的调用栈是这样的: this._constructComponent -> this._constructComponentWithoutOwner

// this._constructComponentWithoutOwner
...
   var Component = this._currentElement.type;
    if (doConstruct) {
      if (__DEV__) {
        return measureLifeCyclePerf(
          () => new Component(publicProps, publicContext, updateQueue),
          this._debugID,
          'ctor',
        );
      } else {
        return new Component(publicProps, publicContext, updateQueue);
      }
    }
...复制代码

根据 _currentElement.type 生成 Component 实例, currentElement.type 即就是继承自 React.Component 的 class 或者纯渲染组件 function.
然后声明 renderElement, 对于 stateless (函数声明的纯渲染组件)组件,renderElement = inst, 否则为 undefined.

接着来看 performInitialMount,

if (renderedElement === undefined) {
  renderedElement = this._renderValidatedComponent();
}
var nodeType = ReactNodeTypes.getType(renderedElement);
this._renderedNodeType = nodeType;
var child = this._instantiateReactComponent(
  renderedElement,
  nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */,
);
this._renderedComponent = child;
var markup = ReactReconciler.mountComponent(
  child,
  transaction,
  hostParent,
  hostContainerInfo,
  this._processChildContext(context),
  debugID,
);复制代码

对于非 stateless 组件时,需要对 renderedElement 赋值。调用栈为: this._renderValidatedComponent -> this._renderValidatedComponentWithoutOwnerOrContext

_renderValidatedComponentWithoutOwnerOrContext: function() {
  var inst = this._instance;
  var renderedElement;
  if (__DEV__) {
    renderedElement = measureLifeCyclePerf(
      () => inst.render(),
      this._debugID,
      'render',
    );
  } else {
    renderedElement = inst.render();
  }
  if (__DEV__) {
    // We allow auto-mocks to proceed as if they're returning null.
    if (renderedElement === undefined && inst.render._isMockFunction) {
      // This is probably bad practice. Consider warning here and
      // deprecating this convenience.
      renderedElement = null;
    }
  }
  return renderedElement;
},复制代码

最终得到 renderedElement 也就是 inst.render() 后的结果。

再往下走 child = this._instantiateReactComponent(renderedElement)markup = ReactReconciler.mountComponent(child,...) .

可以联想到,这里会不断的循环递归调用 ReactReconciler.mountComponent, 知道 child 不是 ReactCompositeComponent 为止,得到最终的 MarkUp. MarkUp 的数据结构可以在 DOMLazyTree 中找到:

// DOMLazyTree
{
  node: node,
  children: [],
  html: null,
  text: null,
  toString,
}复制代码

拿到了 MarkUp,就只剩下最后一步了,通过 ReactMount._mountImageIntoNode 来吧 MarkUp 挂载到实际 DOM 中。

ReactMount._mountImageIntoNode

_mountImageIntoNode: function(
  markup,
  container,
  instance,
  shouldReuseMarkup,
  transaction,
) {
  invariant(
    isValidContainer(container),
    'mountComponentIntoNode(...): Target container is not valid.',
  );
  // 首次渲染,这里为 shouldReuseMarkup = false ,里面都是更新的逻辑
  if (shouldReuseMarkup) {
    ...
  }
  // 首次渲染,transaction.useCreateElement = true
  if (transaction.useCreateElement) {
    while (container.lastChild) {
      container.removeChild(container.lastChild);
    }
    DOMLazyTree.insertTreeBefore(container, markup, null);
  } else {
    setInnerHTML(container, markup);
    ReactDOMComponentTree.precacheNode(instance, container.firstChild);
  }
},复制代码

逻辑很简单,有两种方式把 markup 渲染到 DOM 中:

  • 清空给定的容器组件,然后把 markup 插入到给定的容器中
  • 调用 setInnerHTML 来把 markup 插入到给定容器中,并缓存虚拟 DOM 到实际的 DOM 节点上

对于初次渲染, 会执行第一种方法,清空容器组件,把 markup 挂载到实际的 DOM 中。调用栈:DOMLazyTree.insertTreeBefore -> insertTreeChildren

function insertTreeChildren(tree) {
  if (!enableLazy) {
    return;
  }
  var node = tree.node;
  var children = tree.children;
  if (children.length) {
    for (var i = 0; i < children.length; i++) {
      insertTreeBefore(node, children[i], null);
    }
  } else if (tree.html != null) {
    setInnerHTML(node, tree.html);
  } else if (tree.text != null) {
    setTextContent(node, tree.text);
  }
}复制代码

递归的挂载所有子组件到 DOM 中。

总结

那么到这里整个初次渲染的逻辑就算走完了,整体上可以看到 React 和 ReactDOM 的代码抽象程度非常高,以至于代码阅读起来非常的绕。本文也只限于整体的流程,没有深究细节,因为细节太多了。

最后整理了一个 ReactDOM.render 执行后流程关系图,能一定程度上帮助理解它的整过过程:
图很大,点击放大后也看不清,建议保存到本地后浏览,会看的清晰点。

ReactDOM.render

ReactDOM.render

相关文章