前言

使用react时常常写类似下面的代码:

import ReactDOM from 'react-dom';

ReactDOM.render(
  <h1>Hello, world!</h1>,
  document.getElementById('root')
);

代码1

这里导入的ReactDOM就是packages/react-dom/client/ReactDOM.js中所导出的对象。从文档可见ReactDOM对象有如下几个方法:(ps:从源码看其实还有很多其他方法)

  • render()
  • hydrate()
  • unmountComponentAtNode()
  • findDOMNode()
  • createPortal()

本文只介绍render()方法

代码分析

render方法定义如下:

   render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    invariant(
      // 1
      isValidContainer(container),
      'Target container is not a DOM element.',
    );
    if (__DEV__) {
      warningWithoutStack(
        !container._reactHasBeenPassedToCreateRootDEV,
        'You are calling ReactDOM.render() on a container that was previously ' +
          'passed to ReactDOM.%s(). This is not supported. ' +
          'Did you mean to call root.render(element)?',
        enableStableConcurrentModeAPIs ? 'createRoot' : 'unstable_createRoot',
      );
    }
    // 2
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },

代码2

render方法接收两个必选参数可一个可选参数,结合代码1的调用可知,element是一个ReactElement对象, container是一个dom节点,callback在上面的代码1并没有指定,他是一个可选函数。

这个render方法做的事情比较简单,一是校验container参数,二是调用legacyRenderSubtreeIntoContainer方法并返回。

接下来是legacyRenderSubtreeIntoContainer

// 删除了第一次调ReactDOM.render不会走的分支
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>, // ReactDOM.render 是null
  children: ReactNodeList, // 是一个ReactElement , ReactDOM.render是第一个参数
  container: DOMContainer, // 是一个dom节点, ReactDOM.render是第二个参数
  forceHydrate: boolean, // ReactDOM.render 是false
  callback: ?Function, // ReactDOM.render 是 第三个参数
) {
  if (__DEV__) {
    topLevelUpdateWarnings(container);
  }

  // TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type." Whyyyyyy.
  // 根据type知道, Root type是个对象,包含
  // render方法
  // unmount方法
  // legacy_renderSubtreeIntoContainer 方法
  // createBatch 方法
  // _internalRoot属性
  let root: Root = (container._reactRootContainer: any);
  if (!root) { // ReactDOM.render调用时走这里
      // Initial mount
      // 调用 legacyCreateRootFromDOMContainer 拿 Root
      root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate, // ReactDOM.render是false
    );

    // 在callback加参数
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(root._internalRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    // 这个是packages/react-reconciler/ReactFiberScheduler.js中的方法
    // TOLEARN: 这个里边应该是一些调度过程, 后续再看
    unbatchedUpdates(() => {
      if (parentComponent != null) {
        root.legacy_renderSubtreeIntoContainer(
          parentComponent,
          children,
          callback,
        );
      } else { // ReactDOM.render方法走这里
        // 这里的root.render 返回的是一个叫Work的东西, TOLEARN,这个Work后面再做了解
        root.render(children, callback);
      }
    });
  }
  // getPublicRootInstance是/packages/react-reconciler/ReactFiberReconciler.js中的方法
  // 关于他是返回的一个什么东西, 后面再看, 总之他是我ReactDOM.render方法回调函数的一个参数,
  // 也是返回值
  return getPublicRootInstance(root._internalRoot);
}

legacyRenderSubtreeIntoContainer在第一次render时做了如下事情:

  1. 调用legacyCreateRootFromDOMContainer拿到一个ReactRoot的实例
  2. 在callback中注入一个参数instance
  3. 调用unbatchedUpdates开始一轮调度过程,这个是猜的
  4. 返回instance

关于instance的获取是调用的getPublicRootInstance,getPublicRootInstance是/packages/react-reconciler/ReactFiberReconciler.js中的方法,后面再研究

关于unbatchedUpdates, 这个东西是这个是packages/react-reconciler/ReactFiberScheduler.js中的方法,简单看了看还没太明白

接下来看一下ReactRoot实例的获取

// 删除了__DEV__分支的代码
// 若需要清理container的子节点,清理, 然后new ReactRoot并返回
function legacyCreateRootFromDOMContainer(
  container: DOMContainer,  // dom节点
  forceHydrate: boolean, // false render
): Root {
  // 是否不需要清理container的子元素, 第一次render是false, 即需要清理
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container); // 第一次调用时为false
  // First clear any existing content.
  if (!shouldHydrate) { // 第一次render走这里
    let warned = false;
    let rootSibling;
    // 这里将container的子元素都清理掉了
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not async by default.
  const isConcurrent = false;
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

这个方法比较简单,在第一次调用ReactDOM.render时,shouldHydrate会是false,所以会走到if (!shouldHydrate) 分支里,将container节点的所有子节点都清理掉,最后是new 了一个ReactRoot作为返回值,关于ReactRoot等内容将放到后面的文章分析。

小结

以上是ReactDOM.render的函数调用示意图。

  • 首先在render方法中校验container参数是否合法,然后调用legacyRenderSubtreeIntoContainer
  • 在legacyRenderSubtreeIntoContainer中, 调用legacyCreateRootFromDOMContainer拿到了ReactRoot的一个实例,调用getPublicRootInstance拿到了instance,用于注入到callback,和作为返回值,调用unbatchedUpdates开始调度过程
  • 在legacyCreateRootFromDOMContainer中,首先清理了container中的所有子节点,然后new了一个ReactRoot并返回

TODO

  • unbatchedUpdates是如何调度的
  • ReactRoot是一个什么样的类
  • dom的操作是在哪里
03-05 20:14