抹桥的博客
3770 字
19 分钟
ReactDOM 是如何把组件渲染到 DOM 中的?
2017-09-21

当我们在开发 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

相关文章#

  • {% post_link react-source-code-analyze-1 React 源码浅析之 - 入口文件 %}
  • {% post_link react-source-code-analyze-2 React 源码浅析之 - ReactBaseClasses %}
  • {% post_link react-source-code-analyze-3 React 源码浅析之 - ReactChildren %}
  • {% post_link react-source-code-analyze-4 React 源码浅析之 - ReactElement %}
  • {% post_link react-source-code-analyze-5 React 源码浅析之 - onlyChildren %}
ReactDOM 是如何把组件渲染到 DOM 中的?
https://blog.kisnows.com/2017/09/21/how-react-render-component-to-dom/
作者
Kisnows
发布于
2017-09-21
许可协议
CC BY-NC-ND 4.0