React 源码解读之 ReactDOM.render

1,142 阅读7分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

react 版本:v17.0.3

在《React 源码解读之React应用的2种启动方式》中我们有介绍到:React 启动应用有 legacyConcurrent 两种模式,其中 legacy 模式调用 ReactDOM.render(<App />, rootNode) 启动 React app 应用,接下来我们看看当执行ReactDOM.render()时,React做了什么事情。

render

// react-dom/src/client/ReactDOMLegacy.js

export function render(
  element: React$Element<any>,
  container: Container,
  callback: ?Function,
) {

 	// 删除了 Dev 部分的代码
  return legacyRenderSubtreeIntoContainer(
    null,
    element,
    container,
    false,
    callback,
  );
}

render 函数通常是React app 应用的入口函数,其中element参数是要渲染的 React Element 元素。container 参数是容器,即要渲染的 React Element 元素要挂靠在页面上的哪个根DOM容器下。callback 参数是可选的,在初次渲染或者更新完成后,如果定义了 callback 回调函数,则会执行callback。

在执行ReactDOM.render() 时,会做两件事情:创建fiber和创建update。下面我们分别来看看这两个过程。

创建fiber

在首次执行ReactDOM.render()时,会创建ReactDOMRootFiberRootHostRootFiber 3个全局对象。其中 ReactDOMRoot 是整个应用的根节点,FiberRoot 是要渲染的组件所在组件树的根节点。

ReactDOMRoot 对象是在legacyCreateRootFromDOMContainer() 函数中创建的,legacyCreateRootFromDOMContainer() 函数则是在 legacyRenderSubtreeIntoContainer 函数中被调用的,我们先来看看legacyRenderSubtreeIntoContainer 这个函数。

legacyRenderSubtreeIntoContainer

// react/packages/react-dom/src/client/ReactDOMLegacy.js
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: Container,
  forceHydrate: boolean,
  callback: ?Function,
) {

  // 从container 的 _reactRootContainer 属性上获取 ReactDOMRoot 对象
  let root = container._reactRootContainer;
  let fiberRoot: FiberRoot;
  if (!root) {
    // 首次调用,root 还未初始化,会进入这里初始化 root

    // Initial mount
    // 创建 ReactDOMRoot 对象,初始化 React 应用环境
    // 创建的 ReactDOMRoot 对象会存储在 container 的 _reactRootContainer 属性上
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,  // 参数 container 即为 document.getElementById('root')
      forceHydrate,
    );
    fiberRoot = root;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function () {
        // instance最终指向 children(入参: 如<App/>)生成的dom节点
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    // 更新容器
    flushSync(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {

    // root 已经初始化,二次调用 ReactDom.render 时执行legacyRenderSubtreeIntoContainer,
    // 获取存储在 container 上的 ReactDOMRoot 对象
    fiberRoot = root;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function () {
        // instance最终指向 children(入参: 如<App/>)生成的dom节点
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    // 更新容器
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  // instance最终指向 children(入参: 如<App/>)生成的dom节点
  return getPublicRootInstance(fiberRoot);
}

在 legacyRenderSubtreeIntoContainer 中:

  • 如果是初次调用ReactDOM.render,会调用 legacyCreateRootFromDOMContainer() 函数创建 ReactDOMRoot对象,初始化 React 应用环境。为了快速完成渲染,初次调用ReactDOM.render时的更新是非批量更新。

    // 创建 ReactDOMRoot 对象,初始化 React 应用环境
    // 创建的 ReactDOMRoot 对象会存储在 container 的 _reactRootContainer 属性上
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,  // 参数 container 即为 document.getElementById('root')
      forceHydrate,
    );
    
  • 如果是非初次调用ReactDOM.render,则获取存储在 container 上的 ReactDOMRoot 对象,然后更新 container 容器。

    // root 已经初始化,二次调用 ReactDom.render 时执行legacyRenderSubtreeIntoContainer,
    // 获取存储在 container 上的 ReactDOMRoot 对象
    fiberRoot = root;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function () {
        // instance最终指向 children(入参: 如<App/>)生成的dom节点
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    // 更新容器
    updateContainer(children, fiberRoot, parentComponent, callback);
    
  • 最后通过 getPublicRootInstance 将 reactElement() 和 DOM对象 div#root 关联起来。

    // instance最终指向 children(入参: 如<App/>)生成的dom节点
    return getPublicRootInstance(fiberRoot);
    

接下来看看 legacyCreateRootFromDOMContainer 是如何创建 ReactDOMRoot 对象的。

legacyCreateRootFromDOMContainer

// react/packages/react-dom/src/client/ReactDOMLegacy.js

function legacyCreateRootFromDOMContainer(
  container: Container,
  forceHydrate: boolean,
): FiberRoot {
  // First clear any existing content.
  if (!forceHydrate) {
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }
  }

  // 1、创建 ReactDOMRoot 对象
  const root = createContainer(
    container, // div#root,在调用 ReactDOM.render 时通过 document.getElementById("root") 获取的根节点
    LegacyRoot,  // 全局变量,标记启动模式为 LegacyRoot模式
    forceHydrate,
    null, // hydrationCallbacks
    false, // isStrictMode
    false, // concurrentUpdatesByDefaultOverride,
  );
    
  // 2、给 container(div#root) 标记为 hostRoot,即根节点
  markContainerAsRoot(root.current, container);

  // 3、 在根DOM容器(div#root)上监听事件
  const rootContainerElement =
    container.nodeType === COMMENT_NODE ? container.parentNode : container;
  listenToAllSupportedEvents(rootContainerElement);

  return root;
}

在 legacyCreateRootFromDOMContainer 中,主要做了三件事:

  1. 调用 createContainer 函数创建 ReactDOMRoot对象。

  2. 将根DOM容器 container(div#root) 标记为 hostRoot,即标记为根节点。

  3. 在根DOM容器 container(div#root) 上监听事件。

以上方法都是在 react-dom 包中实现,在 legacyCreateRootFromDOMContainer 中调用了 react-reconciler 包中的 createContainer 方法来创建 ReactDOMRoot对象,可见,react 通过 createContainer 函数将 react-dom包和 react-reconciler包联系了起来。

createContainer

// react/packages/react-reconciler/src/ReactFiberReconciler.new.js

export function createContainer(
  containerInfo: Container,
  tag: RootTag, // LegacyRoot全局变量,标记启动模式为 LegacyRoot模式
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
): OpaqueRoot {
  // 创建 fiberRoot 对象
  return createFiberRoot(
    containerInfo,
    tag,
    hydrate,
    hydrationCallbacks,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );
}

createContainer 函数并没有具体的实现,而是调用了 createFiberRoot 函数来创建 fiberRoot对象。

  • containerInfo 参数就是根DOM容器 div#root,是在调用 ReactDOM.render 时通过 document.getElementById("root") 获取的根DOM节点。

  • tag 参数就是 LegacyRoot全局变量,标记React app的启动模式为 LegacyRoot模式。

createFiberRoot

// react/packages/react-reconciler/src/ReactFiberRoot.new.js
export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
): FiberRoot {
  // 1、创建根DOM容器的 FiberRoot 节点
  // FiberRootNode 类中的 tag 参数是 RootTag,
  // 用来标记 React 应用的启动模式(LegacyRoot 和 ConcurrentRoot 两种模式)
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  // 2、这里创建了`react`应用的首个`fiber`对象, 称为`HostRootFiber`
  const uninitializedFiber = createHostRootFiber(
    tag,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );
  
  // 3、将根DOM容器的fiber节点与 HostRootFiber 关联起来
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  // 4、初始化HostRootFiber的 memoizedState
  if (enableCache) {
    const initialCache = new Map();
    root.pooledCache = initialCache;
    const initialState = {
      element: null,
      cache: initialCache,
    };
    uninitializedFiber.memoizedState = initialState;
  } else {
    const initialState = {
      element: null,
    };
    uninitializedFiber.memoizedState = initialState;
  }

  // 初始化HostRootFiber的updateQueue
  initializeUpdateQueue(uninitializedFiber);

  return root;
}

createFiberRoot 主要做了以下4件事:

  1. 调用 new FiberRootNode() 创建根DOM容器 div#root 的fiber根节点。

  2. 调用 createHostRootFiber 函数创建react应用的首个 fiber 对象,其称为 HostRootFiber。

  3. 将根DOM容器的fiber节点与 HostRootFiber 关联起来。

  4. 初始化HostRootFiber的 memoizedState 和 updateQueue。

createHostRootFiber

// react/packages/react-reconciler/src/ReactFiber.new.js
export function createHostRootFiber(
  tag: RootTag,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
): Fiber {

  // fiber节点mode属性, 会与2种RootTag(ConcurrentRoot, LegacyRoot)关联起来
  let mode;

  // ConcurrentRoot 的启动模式
  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode;
    if (isStrictMode === true) {
      mode |= StrictLegacyMode;

      if (enableStrictEffects) {
        mode |= StrictEffectsMode;
      }
    } else if (enableStrictEffects && createRootStrictEffectsByDefault) {
      mode |= StrictLegacyMode | StrictEffectsMode;
    }
    if (
      // We only use this flag for our repo tests to check both behaviors.
      // TODO: Flip this flag and rename it something like "forceConcurrentByDefaultForTesting"
      !enableSyncDefaultUpdates ||
      // Only for internal experiments.
      (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride)
    ) {
      mode |= ConcurrentUpdatesByDefaultMode;
    }
  } else {
    mode = NoMode;
  }

  if (enableProfilerTimer && isDevToolsPresent) {
    // Always collect profile timings when DevTools are present.
    // This enables DevTools to start capturing timing at any point–
    // Without some nodes in the tree having empty base times.
    mode |= ProfileMode;
  }

  // 创建 fiber 节点,
  // 传入 HostRoot 全局变量,标记为根节点
  // 传入 mode,标记启动类型
  return createFiber(HostRoot, null, null, mode);
}

在 createHostRootFiber 函数中,根据传入的三个参数来初始化 fiber 节点的 mode 属性,mode属性会与两种 RootTag(ConcurrentRoot、LegacyRoot)关联起来。

值得注意的是,fiber树中所有节点的mode都会和HostRootFiber.mode一致(新建的 fiber 节点, 其 mode 来源于父节点),所以HostRootFiber.mode非常重要, 它决定了以后整个 fiber 树的构建过程。

最后,调用 createFiber 函数创建 HostRootFiber 节点。

createFiber

// react/packages/react-reconciler/src/ReactFiber.new.js
const createFiber = function(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
): Fiber {
  // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
  return new FiberNode(tag, pendingProps, key, mode);
};

在 createFiber 中,返回了 FiberNode 的一个实例对象,即返回一个新的fiber节点。

创建update

无论是首次执行 ReactDOM.render 还是多次调用 ReactDOM.render,都会调用 updateContainer函数创建 update 来开启一次更新。

// react-reconciler/src/ReactFiberReconciler.new.js

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  
  // ...

  // container 的current属性保存了更新过程中的一棵fiber树,对应着屏幕上已经渲染好的内容
  // 获取更新过程中的 current树
  const current = container.current;
  // 获取当前时间,通过 performance.now() 或 Date.now() 获取的秒数
  // 其源码在 scheduler/src/forks/Scheduler.js 中的 getCurrentTime 函数中
  const eventTime = requestEventTime();
  // 创建一个优先级变量(lane模型,通常称为车道模型)
  const lane = requestUpdateLane(current);

  if (enableSchedulingProfiler) {
    markRenderScheduled(lane);
  }

  // 获取 context
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  // ...

  // 新建一个 update
  const update = createUpdate(eventTime, lane);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  // update.payload 为需要挂载造根节点的组件
  update.payload = { element };

  // 这里的callback是 legacy启动模式 ReactDOM.render(<App />, document.getElementById('root'), dom => {}); 的 回调函数
  // Concurrent模式 :ReactDOM.createRoot(rootNode).render(<App />) 不支持回调
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    // ...
    update.callback = callback;
  }
  
  // 将新建的 update 添加到 update链表中
  enqueueUpdate(current, update, lane);
  // 进入任务调度
  const root = scheduleUpdateOnFiber(current, lane, eventTime);
  if (root !== null) {
    entangleTransitions(root, current, lane);
  }

  return lane;
}

在 updateContainer 中,调用 createUpdate 创建一个新的 update 后,将该 update 添加到 updateQueue 中(updateQueue是一个环形链表结构),然后通过调用 scheduleUpdateOnFiber 函数,进入任务调度流程。详情请阅读《React 源码解读之任务调度流程

流程图

ReactDOM.render的执行过程,是从 react-dom 包发起,内部调用了 react-reconciler 包,其流程如下:

总结

在执行ReactDOM.render() 时,主要做了两件事情:创建fiber和创建update

如果是首次执行ReactDOM.render,则会创建三个对象,分别是ReactDOMRoot对象、FiberRoot对象、HostRootFiber对象。其中 ReactDOMRoot 是整个应用的根节点,在 legacyCreateRootFromDOMContainer 函数中创建,并在该函数中完成了事件的监听。FiberRoot 是要渲染组件所在组件树的根节点,在 createFiberRoot 函数中创建,作为 react-reconciler 在运行过程中的全局上下文,保存着fiber构建过程中所依赖的全局状态。

在首次执行ReactDOM.render创建完三个对象后,调用 updateContainer 创建update并添加到updateQueue中,然后进入任务调度流程。

如果是非首次执行ReactDOM.render,则直接获取存储在 container 上的 ReactDOMRoot 对象,然后也是一样调用 updateContainer 创建update并添加到updateQueue中,并进入任务调度流程。