接下来我们主要讲解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
源码中, 有 两 种启动方式, 其基本说明如下:
-
legacy
模式:ReactDOM.render(<App />, rootNode)
. 这是当前 React app 之前使用的方式. 这个模式不支持这些新功能(concurrent 支持的所有功能).现在在react@19.0.0默认已经不支持这种形式,LegacyRoot则主要是为了与旧版本的React应用兼容。 -
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根组件的标签。
LegacyRoot (0): 这是最传统的根类型,使用这个标签的根组件会运行在旧的同步渲染模式下。在这种模式下,React在渲染过程中会阻塞用户的交互操作,导致掉帧。这种渲染方式通常用于老版本的React应用或者在不支持并发模式的环境下运行。
ConcurrentRoot (1): 这是React引入的并发渲染模式的根类型。使用这个标签的根组件可以更好地与用户交互同时进行渲染,通过Time Slicing和Suspense等技术,React可以在渲染过程中根据需要中断渲染,处理完用户的交互后再返回继续渲染,从而避免长时间阻塞主线程。这种模式提供了更好的用户体验,特别是在进行了性能优化的应用中。
总的来说,ConcurrentRoot 的引入是为了让React应用可以在更加复杂的现代浏览器环境中运行得更好,支持更为复杂的应用场景和更高的性能要求,而LegacyRoot则主要是为了与旧版本的React应用兼容。在新项目中使用ConcurrentRoot可以获得更好的性能表现。
一、初始化流程
在调用入口函数之前,<App/>
和 DOM 对象div#root
之间没有关联, 用图片表示它们的关系如下所示:
1.1 初始化根节点
无论Legacy或者Concurrent
模式, react 在初始化时, 都会创建 3 个全局对象
ReactDOMRoot
对象
属于react-dom
包, 该对象暴露有render,unmount
方法, 通过调用该实例的render
方法, 可以引导 react 应用的启动.它的定义如下
export type RootType = {
render(children: ReactNodeList): void,
unmount(): void,
_internalRoot: FiberRoot | null,
};
-
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;
......
}
-
HostRootFiber
对象属于
react-reconciler
包, 这是 react 应用中的第一个 Fiber 对象, 是 Fiber 树的根节点, 节点的类型是Fiber
这 3 个对象是 react 体系得以运行的基本保障, 一经创建大多数场景不会再销毁(除非卸载整个应用root.unmount()
).
这一过程是从react-dom
包发起, 内部调用了react-reconciler
包, 核心流程图如下
1.2 创建 ReactDOMRoot 对象
由于现在从源码上追踪, 也对应了 3 种. 最终都 new 一个ReactDOMRoot
实例, legacy模型已经没有一些主要的入口,所以我们后面主要以concurrent模式来逐一解释这 3 个对象的创建过程.
Concurrent
模式和Blocking
模式从调用方式上直接可以看出
- 首先调用 ReactDOM.createRoot 创建
ReactDOMRoot
实例 - 然后调用
ReactDOMRoot
实例的render
方法 - 调用
createContainer
创建FiberRootNode
对象, 并将其挂载到this._internalRoot
上. - 原型上有
render
和unmount
方法, 且内部都会调用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
应用的初始化完毕.
将此刻内存中各个对象的引用情况表示出来:
注意:
此时<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);
};
在调用更新时都会执行updateContainer
. updateContainer
函数串联了react-dom
与react-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-dom
与react-reconciler
. 此处暂时不深入分析updateContainer
函数的具体功能, 需要关注其最后调用了updateContainerImpl
.
所以到此为止, 通过调用react-dom
包的api
(如: ReactDOM.render
), react
内部经过一系列运转, 完成了初始化, 并且进入了reconciler 运作流程
的第一个阶段.
三、总结
本章节介绍了react
应用的启动方式. 分析了启动后创建了 3 个关键对象, 并绘制了对象在内存中的引用关系. 启动过程最后调用updateContainer
进入react-reconciler
包,它会负责处理实际的更新逻辑,包括创建或更新 Fiber 树、调度工作任务等