四、深入浅出React源码:应用初始化分析

86 阅读5分钟

接下来我们主要讲解react应用程序的启动过程, 位于react-dom

在正式分析源码之前, 我们先看一段代码,先了解一下react应用的初始化过程

    import React from 'react';

    import ReactDOM from 'react-dom/client';

    import App from './App';
    // 1. 创建ReactDOMRoot对象
    const root = ReactDOM.createRoot(document.getElementById('root'));

    root.render(
        <React.StrictMode>
            <App />
        </React.StrictMode>
    );

在当前稳定版react@19.0.0源码中, 有 两 种启动方式, 其基本说明如下:

  1. legacy 模式: ReactDOM.render(<App />, rootNode). 这是当前 React app 之前使用的方式. 这个模式不支持这些新功能(concurrent 支持的所有功能).现在在react@19.0.0默认已经不支持这种形式,LegacyRoot则主要是为了与旧版本的React应用兼容。

  2. Concurrent 模式ReactDOM.createRoot(rootNode).render(<App />). 目前是React 的默认开发模式. 这个模式开启了所有的新功能.

    // ConcurrentRoot
    // 1. 创建ReactDOMRoot对象
    const reactDOMRoot = ReactDOM.createRoot(document.getElementById('root'));
    // 2. 调用render
    reactDOMRoot.render(<App />); // 不支持回调
    

在React内部,LegacyRoot 和 ConcurrentRoot 是用来区分不同类型的React根组件的标签。

image.png

LegacyRoot (0): 这是最传统的根类型,使用这个标签的根组件会运行在旧的同步渲染模式下。在这种模式下,React在渲染过程中会阻塞用户的交互操作,导致掉帧。这种渲染方式通常用于老版本的React应用或者在不支持并发模式的环境下运行。

ConcurrentRoot (1): 这是React引入的并发渲染模式的根类型。使用这个标签的根组件可以更好地与用户交互同时进行渲染,通过Time Slicing和Suspense等技术,React可以在渲染过程中根据需要中断渲染,处理完用户的交互后再返回继续渲染,从而避免长时间阻塞主线程。这种模式提供了更好的用户体验,特别是在进行了性能优化的应用中。

总的来说,ConcurrentRoot 的引入是为了让React应用可以在更加复杂的现代浏览器环境中运行得更好,支持更为复杂的应用场景和更高的性能要求,而LegacyRoot则主要是为了与旧版本的React应用兼容。在新项目中使用ConcurrentRoot可以获得更好的性能表现。

一、初始化流程

在调用入口函数之前,<App/>和 DOM 对象div#root之间没有关联, 用图片表示它们的关系如下所示:

image.png

1.1 初始化根节点

无论Legacy或者Concurrent模式, react 在初始化时, 都会创建 3 个全局对象

  1. ReactDOMRoot对象

属于react-dom包, 该对象暴露有render,unmount方法, 通过调用该实例的render方法, 可以引导 react 应用的启动.它的定义如下

    export type RootType = {

        render(children: ReactNodeList): void,

        unmount(): void,

        _internalRoot: FiberRoot | null,

     };
  1. FiberRootNode对象

    • 属于react-reconciler包, 作为react-reconciler在运行过程中的全局上下文, 保存 fiber 构建过程中所依赖的全局状态.
    • 其大部分实例变量用来存储fiber 构造循环过程的各种状态.react 应用内部, 可以根据这些实例变量的值, 控制执行逻辑.
function FiberRootNode(

        this: $FlowFixMe,

        containerInfo: any,

        // $FlowFixMe[missing-local-annot]

        tag,

        hydrate: any,

        identifierPrefix: any,

        onUncaughtError: any,

        onCaughtError: any,

        onRecoverableError: any,

        formState: ReactFormState<any, any> | null,

        ) {

        this.tag = disableLegacyMode ? ConcurrentRoot : tag;

        this.containerInfo = containerInfo;

        this.pendingChildren = null;

        this.current = null;

        this.pingCache = null;

        this.timeoutHandle = noTimeout;

        this.cancelPendingCommit = null;

        this.context = null;

        this.pendingContext = null;

        this.next = null;

        this.callbackNode = null;

        this.callbackPriority = NoLane;

        this.expirationTimes = createLaneMap(NoTimestamp);

        this.pendingLanes = NoLanes;

        this.suspendedLanes = NoLanes;

        this.pingedLanes = NoLanes;

        this.warmLanes = NoLanes;

        this.expiredLanes = NoLanes;

        this.errorRecoveryDisabledLanes = NoLanes;

        this.shellSuspendCounter = 0;

        this.entangledLanes = NoLanes;

        this.entanglements = createLaneMap(NoLanes);

        this.hiddenUpdates = createLaneMap(null);

        this.identifierPrefix = identifierPrefix;

        this.onUncaughtError = onUncaughtError;

        this.onCaughtError = onCaughtError;

        this.onRecoverableError = onRecoverableError;

        this.pooledCache = null;

        this.pooledCacheLanes = NoLanes;
        ......
}
  1. HostRootFiber对象

    属于react-reconciler包, 这是 react 应用中的第一个 Fiber 对象, 是 Fiber 树的根节点, 节点的类型是Fiber

这 3 个对象是 react 体系得以运行的基本保障, 一经创建大多数场景不会再销毁(除非卸载整个应用root.unmount()).

这一过程是从react-dom包发起, 内部调用了react-reconciler包, 核心流程图如下

image.png

1.2 创建 ReactDOMRoot 对象

由于现在从源码上追踪, 也对应了 3 种. 最终都 new 一个ReactDOMRoot实例, legacy模型已经没有一些主要的入口,所以我们后面主要以concurrent模式来逐一解释这 3 个对象的创建过程.

Concurrent模式和Blocking模式从调用方式上直接可以看出

  1. 首先调用 ReactDOM.createRoot 创建ReactDOMRoot实例
  2. 然后调用ReactDOMRoot实例的render方法
  3. 调用createContainer创建FiberRootNode对象, 并将其挂载到this._internalRoot上.
  4. 原型上有renderunmount方法, 且内部都会调用updateContainer进行更新.
/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {ReactNodeList, ReactFormState} from 'shared/ReactTypes';
import type {
  FiberRoot,
  TransitionTracingCallbacks,
} from 'react-reconciler/src/ReactInternalTypes';

import {isValidContainer} from 'react-dom-bindings/src/client/ReactDOMContainer';
import {queueExplicitHydrationTarget} from 'react-dom-bindings/src/events/ReactDOMEventReplaying';
import {REACT_ELEMENT_TYPE} from 'shared/ReactSymbols';

export type RootType = {
  render(children: ReactNodeList): void,
  unmount(): void,
  _internalRoot: FiberRoot | null,
};

export type CreateRootOptions = {
  unstable_strictMode?: boolean,
  unstable_transitionCallbacks?: TransitionTracingCallbacks,
  identifierPrefix?: string,
  onUncaughtError?: (
    error: mixed,
    errorInfo: {+componentStack?: ?string},
  ) => void,
  onCaughtError?: (
    error: mixed,
    errorInfo: {
      +componentStack?: ?string,
      +errorBoundary?: ?React$Component<any, any>,
    },
  ) => void,
  onRecoverableError?: (
    error: mixed,
    errorInfo: {+componentStack?: ?string},
  ) => void,
};

export type HydrateRootOptions = {
  // Hydration options
  onHydrated?: (suspenseNode: Comment) => void,
  onDeleted?: (suspenseNode: Comment) => void,
  // Options for all roots
  unstable_strictMode?: boolean,
  unstable_transitionCallbacks?: TransitionTracingCallbacks,
  identifierPrefix?: string,
  onUncaughtError?: (
    error: mixed,
    errorInfo: {+componentStack?: ?string},
  ) => void,
  onCaughtError?: (
    error: mixed,
    errorInfo: {
      +componentStack?: ?string,
      +errorBoundary?: ?React$Component<any, any>,
    },
  ) => void,
  onRecoverableError?: (
    error: mixed,
    errorInfo: {+componentStack?: ?string},
  ) => void,
  formState?: ReactFormState<any, any> | null,
};

import {
  isContainerMarkedAsRoot,
  markContainerAsRoot,
  unmarkContainerAsRoot,
} from 'react-dom-bindings/src/client/ReactDOMComponentTree';
import {listenToAllSupportedEvents} from 'react-dom-bindings/src/events/DOMPluginEventSystem';
import {COMMENT_NODE} from 'react-dom-bindings/src/client/HTMLNodeType';

import {
  createContainer,
  createHydrationContainer,
  updateContainer,
  updateContainerSync,
  flushSyncWork,
  isAlreadyRendering,
  defaultOnUncaughtError,
  defaultOnCaughtError,
  defaultOnRecoverableError,
} from 'react-reconciler/src/ReactFiberReconciler';
import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';

// $FlowFixMe[missing-this-annot]
function ReactDOMRoot(internalRoot: FiberRoot) {
  this._internalRoot = internalRoot;
}

// $FlowFixMe[prop-missing] found when upgrading Flow
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render =
  // $FlowFixMe[missing-this-annot]
  function (children: ReactNodeList): void {
    const root = this._internalRoot;
    if (root === null) {
      throw new Error('Cannot update an unmounted root.');
    }

    if (__DEV__) {
      // using a reference to `arguments` bails out of GCC optimizations which affect function arity
      const args = arguments;
      if (typeof args[1] === 'function') {
        console.error(
          'does not support the second callback argument. ' +
            'To execute a side effect after rendering, declare it in a component body with useEffect().',
        );
      } else if (isValidContainer(args[1])) {
        console.error(
          'You passed a container to the second argument of root.render(...). ' +
            "You don't need to pass it again since you already passed it to create the root.",
        );
      } else if (typeof args[1] !== 'undefined') {
        console.error(
          'You passed a second argument to root.render(...) but it only accepts ' +
            'one argument.',
        );
      }
    }
    updateContainer(children, root, null, null);
  };

// $FlowFixMe[prop-missing] found when upgrading Flow
ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount =
  // $FlowFixMe[missing-this-annot]
  function (): void {
    if (__DEV__) {
      // using a reference to `arguments` bails out of GCC optimizations which affect function arity
      const args = arguments;
      if (typeof args[0] === 'function') {
        console.error(
          'does not support a callback argument. ' +
            'To execute a side effect after rendering, declare it in a component body with useEffect().',
        );
      }
    }
    const root = this._internalRoot;
    if (root !== null) {
      this._internalRoot = null;
      const container = root.containerInfo;
      if (__DEV__) {
        if (isAlreadyRendering()) {
          console.error(
            'Attempted to synchronously unmount a root while React was already ' +
              'rendering. React cannot finish unmounting the root until the ' +
              'current render has completed, which may lead to a race condition.',
          );
        }
      }
      updateContainerSync(null, root, null, null);
      flushSyncWork();
      unmarkContainerAsRoot(container);
    }
  };

export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  if (!isValidContainer(container)) {
    throw new Error('Target container is not a DOM element.');
  }

  warnIfReactDOMContainerInDEV(container);

  const concurrentUpdatesByDefaultOverride = false;
  let isStrictMode = false;
  let identifierPrefix = '';
  let onUncaughtError = defaultOnUncaughtError;
  let onCaughtError = defaultOnCaughtError;
  let onRecoverableError = defaultOnRecoverableError;
  let transitionCallbacks = null;

  if (options !== null && options !== undefined) {
    if (__DEV__) {
      if ((options: any).hydrate) {
        console.warn(
          'hydrate through createRoot is deprecated. Use ReactDOMClient.hydrateRoot(container, <App />) instead.',
        );
      } else {
        if (
          typeof options === 'object' &&
          options !== null &&
          (options: any).$$typeof === REACT_ELEMENT_TYPE
        ) {
          console.error(
            'You passed a JSX element to createRoot. You probably meant to ' +
              'call root.render instead. ' +
              'Example usage:\n\n' +
              '  let root = createRoot(domContainer);\n' +
              '  root.render(<App />);',
          );
        }
      }
    }
    if (options.unstable_strictMode === true) {
      isStrictMode = true;
    }
    if (options.identifierPrefix !== undefined) {
      identifierPrefix = options.identifierPrefix;
    }
    if (options.onUncaughtError !== undefined) {
      onUncaughtError = options.onUncaughtError;
    }
    if (options.onCaughtError !== undefined) {
      onCaughtError = options.onCaughtError;
    }
    if (options.onRecoverableError !== undefined) {
      onRecoverableError = options.onRecoverableError;
    }
    if (options.unstable_transitionCallbacks !== undefined) {
      transitionCallbacks = options.unstable_transitionCallbacks;
    }
  }

  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onUncaughtError,
    onCaughtError,
    onRecoverableError,
    transitionCallbacks,
  );
  markContainerAsRoot(root.current, container);

  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;
  listenToAllSupportedEvents(rootContainerElement);

  // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
  return new ReactDOMRoot(root);
}

// $FlowFixMe[missing-this-annot]
function ReactDOMHydrationRoot(internalRoot: FiberRoot) {
  this._internalRoot = internalRoot;
}
function scheduleHydration(target: Node) {
  if (target) {
    queueExplicitHydrationTarget(target);
  }
}
// $FlowFixMe[prop-missing] found when upgrading Flow
ReactDOMHydrationRoot.prototype.unstable_scheduleHydration = scheduleHydration;

export function hydrateRoot(
  container: Document | Element,
  initialChildren: ReactNodeList,
  options?: HydrateRootOptions,
): RootType {
  if (!isValidContainer(container)) {
    throw new Error('Target container is not a DOM element.');
  }

  warnIfReactDOMContainerInDEV(container);

  if (__DEV__) {
    if (initialChildren === undefined) {
      console.error(
        'Must provide initial children as second argument to hydrateRoot. ' +
          'Example usage: hydrateRoot(domContainer, <App />)',
      );
    }
  }

  // For now we reuse the whole bag of options since they contain
  // the hydration callbacks.
  const hydrationCallbacks = options != null ? options : null;

  const concurrentUpdatesByDefaultOverride = false;
  let isStrictMode = false;
  let identifierPrefix = '';
  let onUncaughtError = defaultOnUncaughtError;
  let onCaughtError = defaultOnCaughtError;
  let onRecoverableError = defaultOnRecoverableError;
  let transitionCallbacks = null;
  let formState = null;
  if (options !== null && options !== undefined) {
    if (options.unstable_strictMode === true) {
      isStrictMode = true;
    }
    if (options.identifierPrefix !== undefined) {
      identifierPrefix = options.identifierPrefix;
    }
    if (options.onUncaughtError !== undefined) {
      onUncaughtError = options.onUncaughtError;
    }
    if (options.onCaughtError !== undefined) {
      onCaughtError = options.onCaughtError;
    }
    if (options.onRecoverableError !== undefined) {
      onRecoverableError = options.onRecoverableError;
    }
    if (options.unstable_transitionCallbacks !== undefined) {
      transitionCallbacks = options.unstable_transitionCallbacks;
    }
    if (options.formState !== undefined) {
      formState = options.formState;
    }
  }

  const root = createHydrationContainer(
    initialChildren,
    null,
    container,
    ConcurrentRoot,
    hydrationCallbacks,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onUncaughtError,
    onCaughtError,
    onRecoverableError,
    transitionCallbacks,
    formState,
  );
  markContainerAsRoot(root.current, container);
  // This can't be a comment node since hydration doesn't work on comment nodes anyway.
  listenToAllSupportedEvents(container);

  // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
  return new ReactDOMHydrationRoot(root);
}

function warnIfReactDOMContainerInDEV(container: any) {
  if (__DEV__) {
    if (isContainerMarkedAsRoot(container)) {
      if (container._reactRootContainer) {
        console.error(
          'You are calling ReactDOMClient.createRoot() on a container that was previously ' +
            'passed to ReactDOM.render(). This is not supported.',
        );
      } else {
        console.error(
          'You are calling ReactDOMClient.createRoot() on a container that ' +
            'has already been passed to createRoot() before. Instead, call ' +
            'root.render() on the existing root instead if you want to update it.',
        );
      }
    }
  }
}




1.3 创建 FiberRootNode 对象

ReactDOMRoot创建过程中, 都会调用一个相同的函数createContainer, 查看后续的函数调用, 最后会创建FiberRootNode 对象(在这个过程中, 特别注意RootTag的传递过程):

  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onUncaughtError,
    onCaughtError,
    onRecoverableError,
    transitionCallbacks,
  );

  const root: FiberRoot = (new FiberRootNode(
    containerInfo,
    tag,
    hydrate,
    identifierPrefix,
    onUncaughtError,
    onCaughtError,
    onRecoverableError,
    formState,
  ): any);

1.4 创建 HostRootFiber 对象

createFiberRoot中, 创建了react应用的首个fiber对象, 称为HostRootFiber(fiber.tag = HostRoot)

    export function createHostRootFiber(
          tag: RootTag,
          isStrictMode: boolean,
        ): Fiber {
          let mode;
          if (disableLegacyMode || tag === ConcurrentRoot) {
            mode = ConcurrentMode;
            if (isStrictMode === true) {
              mode |= StrictLegacyMode | StrictEffectsMode;
            }
          } 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;
          }

          return createFiber(HostRoot, null, null, mode);
}

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

运行到这里, react应用的初始化完毕.

将此刻内存中各个对象的引用情况表示出来:

image.png

注意:

此时<App/>还是独立在外的, 还没有和目前创建的 3 个全局对象关联起来

二、调用更新入口

ReactDOMRoot原型上有render方法

    ReactDOMRoot.prototype.render = function (children: ReactNodeList): void {
           const root = this._internalRoot;
           if (root === null) {
               throw new Error('Cannot update an unmounted root.');
           }
           if (__DEV__) {

               // using a reference to `arguments` bails out of GCC optimizations which affect function arity

               const args = arguments;

           if (typeof args[1] === 'function') {

               console.error(

               'does not support the second callback argument. ' +

               'To execute a side effect after rendering, declare it in a component body with useEffect().',

           );

           } else if (isValidContainer(args[1])) {

               console.error(

               'You passed a container to the second argument of root.render(...). ' +

               "You don't need to pass it again since you already passed it to create the root.",

           );

           } else if (typeof args[1] !== 'undefined') {

               console.error(

               'You passed a second argument to root.render(...) but it only accepts ' +

               'one argument.',);
            }
            }
           updateContainer(children, root, null, null);
};

在调用更新时都会执行updateContainerupdateContainer函数串联了react-domreact-reconciler, 之后的逻辑进入了react-reconciler包.

继续跟踪updateContainer函数

    export function updateContainer(
      element: ReactNodeList,
      container: OpaqueRoot,
      parentComponent: ?React$Component<any, any>,
      callback: ?Function,
    ): Lane {
       // 1. 获取当前时间戳, 计算本次更新的优先级
      const current = container.current;
      const lane = requestUpdateLane(current);
      updateContainerImpl(
        current,
        lane,
        element,
        container,
        parentComponent,
        callback,
      );
      return lane;
}

updateContainer函数位于react-reconciler包中, 它串联了react-domreact-reconciler. 此处暂时不深入分析updateContainer函数的具体功能, 需要关注其最后调用了updateContainerImpl.

所以到此为止, 通过调用react-dom包的api(如: ReactDOM.render), react内部经过一系列运转, 完成了初始化, 并且进入了reconciler 运作流程的第一个阶段.

三、总结

本章节介绍了react应用的启动方式. 分析了启动后创建了 3 个关键对象, 并绘制了对象在内存中的引用关系. 启动过程最后调用updateContainer进入react-reconciler包,它会负责处理实际的更新逻辑,包括创建或更新 Fiber 树、调度工作任务等