React 源码解读之React应用的2种启动方式

1,364 阅读8分钟

react 版本:v17.0.3

前言

在官网中,介绍了3种React应用的启动方式,我们简单地回顾下这三种方式。

1、legacy 模式

ReactDOM.render(<App />, rootNode) 这是当前 React app 使用的方式,这个模式可能不支持这些新功能(concurrent 支持的所有功能)

// LegacyRoot
ReactDOM.render(<App />, document.getElementById('root'), dom => {}); // 支持callback回调, 参数是一个dom对象

2、Blocking 模式

ReactDOM.createBlockingRoot(rootNode).render(<App />)。目前正在实验中, 它仅提供了 concurrent 模式的小部分功能,作为迁移到 concurrent 模式的第一个步骤。

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

3、Concurrent 模式

ReactDOM.createRoot(rootNode).render(<App />)。目前在v18.0.0-alpha,和experiment版本中发布,这个模式开启了所有的新功能。

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

而在 React 最新的源码v17.0.2中,已经删除了 Blocking的启动方式,只剩下 legacy 和 Concurrent 两种启动模式。因此,本文只介绍这两种启动模式。

注意:React官网显示的 v17.0.2 版本,在源码中实际上是对应了 v17.0.3 版本。在 v17.0.1 版本的源码中提供了 legacy、Blocking 和 Concurrent 三种启动模式。而在v17.0.3 版本中,只提供了 legacy 和 Concurrent 两种启动模式。下文的源码解析均是基于 v17.0.3 版本。

ReactRootTag.js 文件中,定义了两个全局变量,用来标记React应用的启动模式。

其中 LegacyRoot 模式调用 ReactDOM.render(<App />, rootNode) 启动应用。ConcurrentRoot 模式调用 ReactDOM.createRoot(rootNode).render(<App />) 启动应用。

// packages/react-reconciler/src/ReactRootTags.js
export type RootTag = 0 | 1;

export const LegacyRoot = 0;
export const ConcurrentRoot = 1;

legacy 模式

legacy 模式调用 ReactDOM.render(, rootNode) 启动 React app 应用,这是当前 React app 使用的方式。

我们来看看 ReactDOM.render 方法:

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

export {
 
  render,
  
};

render 方法只是在 react/packages/react-dom/src/client/ReactDOM.js 中对外导出,真正的实现在react/packages/react-dom/src/client/ReactDOMLegacy.js 文件中。

render

// react/packages/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 方法中并没有具体的实现,只是调用了 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 应用环境,并通过 getPublicRootInstance 将 reactElement() 和 DOM对象 div#root 关联起来。

  • 如果是非初次调用ReactDOM.render,则获取存储在 container 上的 ReactDOMRoot 对象,然后更新 container 容器。

接下来看看 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);
};

运行到这里,legacy 模式的启动过程就结束了。在这个过程中,主要创建了3个全局对象。

  1. ReactDOMRoot 对象

该对象属于 react-dom 包,在 legacyCreateRootFromDOMContainer 函数中创建,并在该函数中完成了事件的监听。

  1. FiberRoot 对象

该对象属于 react-reconciler 包,在 createFiberRoot 函数中创建,作为 react-reconciler 在运行过程中的全局上下文,保存着fiber构建过程中所依赖的全局状态。

  1. HostRootFiber 对象

该对象属于 react-reconciler 包,在 createHostRootFiber 函数中创建,这是 react 应用中的第一个 Fiber 对象, 是 Fiber 树的根节点。

流程图

legacy 模式的启动过程,是从 react-dom 包发起,内部调用了 react-reconciler 包,其流程如下:

concurrent 模式

ConcurrentRoot 模式调用ReactDOM.createRoot(rootNode).render(<App />)。目前在v18.0.0-alpha,和experiment版本中发布,这个模式开启了所有的新功能。

我们来看看 ReactDOM.createRoot 方法。

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

export {
  
  createRoot,
  
};

createRoot 方法只是在 react/packages/react-dom/src/client/ReactDOM.js 中对外导出,真正的实现在 react/packages/react-dom/src/client/ReactDOMRoot.js 文件中。

createRoot

// react/packages/react-dom/src/client/ReactDOMRoot.js
export function createRoot(
  container: Container,
  options?: CreateRootOptions,
): RootType {
  if (!isValidContainerLegacy(container)) {
    throw new Error('createRoot(...): Target container is not a DOM element.');
  }

  warnIfReactDOMContainerInDEV(container);

  // TODO: Delete these options
  const hydrate = options != null && options.hydrate === true;
  const hydrationCallbacks =
    (options != null && options.hydrationOptions) || null;
  const mutableSources =
    (options != null &&
      options.hydrationOptions != null &&
      options.hydrationOptions.mutableSources) ||
    null;
  // END TODO

  const isStrictMode = options != null && options.unstable_strictMode === true;
  let concurrentUpdatesByDefaultOverride = null;
  if (allowConcurrentByDefault) {
    concurrentUpdatesByDefaultOverride =
      options != null && options.unstable_concurrentUpdatesByDefault != null
        ? options.unstable_concurrentUpdatesByDefault
        : null;
  }

  // 1、创建fiberRoot对象,该fiberRoot对象将会被挂载到 ReactDOMRoot 对象的_internalRoot属性上
  const root = createContainer(
    container,
    ConcurrentRoot,
    hydrate,
    hydrationCallbacks,
    isStrictMode,
    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);

  // TODO: Delete this path
  if (mutableSources) {
    for (let i = 0; i < mutableSources.length; i++) {
      const mutableSource = mutableSources[i];
      registerMutableSourceForHydration(root, mutableSource);
    }
  }
  // END TODO

  // 4、返回 ReactDoRoot 实例
  return new ReactDOMRoot(root);
}

在 createRoot 函数中,主要做了四件事情:

  1. 调用 react-reconciler 包的 createContainer 创建 FiberRoot 对象,该 FiberRoot 对象将会被挂载到 ReactDOMRoot 对象的 _internalRoot 属性上。在创建 FiberRoot 对象的过程中,还会创建 HostRootFiber 对象。

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

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

  4. 调用 new ReactDOMRoot(root),实例化 ReactDOMRoot 对象,并将传入的 FiberRoot 对象挂载到实例属性_internalRoot上。

接下来,我们来看看 创建 FiberRoot 对象的 createContainer 函数。

createContainer

ConcurrentRoot 模式中创建FiberRoot对象的createContainer函数与 Legacy模式中创建FiberRoot对象的createContainer函数是同一个函数,详细解析请阅读 legacy 模式章节的 createContainer 小节

createFiberRoot

ConcurrentRoot 模式中创建FiberRoot对象的createFiberRoot函数与Legacy模式中创建FiberRoot对象的createFiberRoot函数是同一个函数,详细解析请阅读 legacy 模式章节的 createFiberRoot 小节。

createHostRootFiber

ConcurrentRoot 模式中创建HostRootFiber对象的createHostRootFiber函数与 Legacy模式中创建HostRootFiber对象的createHostRootFiber函数是同一个函数,详细解析请阅读 legacy 模式章节的 createHostRootFiber 小节。

createFiber

ConcurrentRoot 模式中实例化 FiberNode节点的createFiber函数与 Legacy模式中实例化FiberNode节点的createFiber函数是同一个函数,详细解析请阅读 legacy 模式章节的 createFiber 小节。

ConcurrentRoot 模式的启动过程运行到这里就结束了。在这个过程中,也创建了 3 个全局对象,这3个对象与LegacyRoot 模式启动过程中创建的3个全局对象是一样的,这里不再赘述。

ReactDOM.render

ConcurrentRoot 模式中,调用 crateRoot 方法后,还需要调用 ReactDOMRoot 原型上的 render 方法进行更新。

// react/packages/react-dom/src/client/ReactDOMRoot.js
ReactDOMRoot.prototype.render = function (children: ReactNodeList): void {

  const root = this._internalRoot;
  if (root === null) {
    throw new Error('Cannot update an unmounted root.');
  }

  // 删除了Dev部分的代码

  updateContainer(children, root, null, null);
};

可见,ConcurrentRoot 模式中,调用 crateRoot 方法创建3个对象后,实际上还需要调用 updateContainer 进行更新。

流程图

ConcurrentRoot 模式的启动过程,也是从 react-dom 包发起,内部调用了 react-reconciler 包,其流程如下:

对比

启动过程的差异

legacy模式与Concurrent模式两者启动过程中的差异在于 ReactDOMRoot 对象的创建。

legacy模式是在 legacyCreateRootFromDOMContainer 函数中创建 ReactDOMRoot 对象,严格意义上来说 legacy模式的 ReactDOMRoot 对象就是 FiberRoot 对象。

Concurrent 模式的 ReactDOMRoot 对象是在 createRoot 函数中实例化的 ReactDOMRoot 实例,该实例对象上有 render 方法。并通过 _internalRoot 属性将 FiberRoot 对象挂载到 ReactDOMRoot 实例上。

两者启动过程流程如下:

对象的引用关系

无论是legacy模式还是Concurrent模式,在启动的过程中都会创建ReactDOMRoot、FiberRoot、HostFiberRoot 三个对象,这三个对象在内存中的引用关系如下:

  • legacy 模式

  • Concurrent 模式

更新过程的差异

1、legacy模式中,首次初始化 root 时是调用 flushSync 更新容器的:

// Initial mount should not be batched.
// 更新容器
flushSync(() => {
  updateContainer(children, fiberRoot, parentComponent, callback);
});

如果root已经初始化,则直接调用 updateContainer 更新容器:

// Update
// 更新容器
updateContainer(children, fiberRoot, parentComponent, callback);

2、Concurrent模式中,在调用 createRoot 创建 ReactDOMRoot 对象后,再调用 ReactDOMRoot 原型上的 render 方法更新容器:

ReactDOM.createBlockingRoot(rootNode).render(<App />)

render 方法实际上也是调用 updateContainer 更新容器:

ReactDOMRoot.prototype.render = function (children: ReactNodeList): void {

  const root = this._internalRoot;
  
  // 删除了 Dev 部分的代码
 
  updateContainer(children, root, null, null);
};

总结

本文详细介绍了React应用启动的两种模式:legacy模式和Concurrent模式,这两种模式在启动的过程中都会创建ReactDOMRoot、FiberRoot、HostFiberRoot 三个对象。

这两种模式中 ReactDOMRoot 对象的创建方式是不一样的,legacy模式是在 legacyCreateRootFromDOMContainer 函数中创建的,实际上 ReactDOMRoot 对象就是 FiberRoot 对象。Concurrent 模式是在 createRoot 函数中实例化了 ReactDOMRoot 实例,并且 ReactDOMRoot 原型上有 render 方法。

ReactDOMRoot、FiberRoot、HostFiberRoot 这三个对象在内存中的引用关系也不是完全相同。legacy模式中通过 ReactDOMRoot 对象的 _reactRootContainer 属性与跟容器div#root 关联在一起,而在 Concurrent模式中是通过 ReactDOMRoot 对象的 _internalRoot 属性与 FiberRoot 对象关联在一起。