Appearance
React 应用启动流程
React 应用的生命周期始于其初始化过程。无论是已经被删除的ReactDOM.hydrate
、 ReactDOM.render
, 还是现代的 createRoot
、hydrateRoot
API,它们都扮演着将 React 组件树"挂载"到真实 DOM 上的关键角色。本章将深入探讨 React 应用的启动流程,从用户调用 API 到内部 Fiber 树的构建,以及初次渲染的概览。
入口函数
createRoot
在 React 17 之前,我们通常使用 ReactDOM.render
来启动一个 React 应用:
jsx
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<App />,
document.getElementById('root')
);
而在 React 18 引入并发特性后,推荐使用 createRoot
,甚至在react19中,将 ReactDOM.render
给移除。
jsx
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
无论使用哪种方式,它们都是 React 应用的“入口点”。当这些方法被调用时,React 会开始一系列复杂的内部操作,将我们用 JSX 编写的组件描述转换为浏览器能够理解和渲染的实际 DOM 结构。这个过程的起点,就是将传入的 React Element(例如 <App />
)转化为内部的 Fiber 树结构。
createRoot
函数是 React 18 及以后版本中创建应用根节点的标准 API,其主要职责是初始化 React 应用的运行环境并返回一个可用于渲染和卸载组件的 ReactDOMRoot 对象。
主要做的事情:
- 接收容器: 它接收一个真实的 DOM 元素作为参数,这个 DOM 元素就是你的 React 应用将会被挂载的地方(比如
document.getElementById('root')
)。 - 创建 React 根 (FiberRoot):
- 它会在内部创建一个
FiberRootNode
对象。你可以把FiberRootNode
想象成整个 React 应用的“大脑”或“总控制室”,它存储了应用的顶层状态、待处理的更新、当前的 Fiber 树等核心信息。 - 同时,它会创建一个特殊的 Fiber 节点,叫做
HostRootFiber
。这个HostRootFiber
是你的组件树在 Fiber 架构中的最顶层节点。 FiberRootNode
和HostRootFiber
会相互引用,建立起 React 内部管理结构的基础。
- 它会在内部创建一个
- 启用并发特性:
createRoot
创建的根默认支持 React 的并发特性 (Concurrent Mode),这意味着 React 可以更智能地安排渲染工作,提高应用的响应性。 - 设置事件系统: 它会为指定的 DOM 容器初始化 React 的事件系统,确保组件中的事件处理器能够正常工作,这个将在后续的章节详细讲解。
- 返回 Root 对象:
createRoot
会返回一个ReactDOMRoot
对象。这个对象上有一个重要的方法:root.render(<App />)
: 你可以使用这个方法来首次渲染你的 React 组件到 DOM 容器中,或者在之后进行更新。root.unmount()
: 用于卸载整个 React 应用。
简单来说,createRoot
就是告诉 React:“嘿,在这个 DOM 节点里,给我创建一个全新的、支持并发渲染的 React 应用吧!”
主要区别总结:
特性 | createRoot | hydrateRoot |
---|---|---|
使用场景 | 客户端纯渲染 (CSR) | 服务端渲染后的客户端激活 (SSR Hydration) |
DOM 容器初始状态 | 通常为空,或不包含 React 渲染的内容 | 包含由服务端预渲染的 HTML 结构 |
DOM 操作 | 创建新的 DOM 节点来构建 UI | 尝试复用已有的 DOM 节点,并附加事件监听器 |
核心目标 | 从无到有构建和渲染 UI | 使服务端静态 HTML 变得可交互,并与客户端 React 状态同步 |
内部标记 | hydrate 标记为 false (默认) | hydrate 标记为 true |
createRoot内部实现
javascript
function createRoot(container, options) {
// 1. 验证容器是否有效
if (!isValidContainer(container)) {
throw new Error('Target container is not a DOM element.');
}
// 2. 初始化选项 (options)
// 3. 创建 FiberRootNode (核心)
const root = createContainer(
container, // DOM 容器节点
ConcurrentRoot, // 根节点类型 (并发模式)
null, // hydrationCallbacks (非 hydrate 模式)
isStrictMode, // 是否严格模式
false, // concurrentUpdatesByDefaultOverride
identifierPrefix, // 标识符前缀
onUncaughtError, // 未捕获错误处理
onCaughtError, // 已捕获错误处理 (来自错误边界)
onRecoverableError, // 可恢复错误处理
transitionCallbacks,// 过渡回调
null // formState
);
// 4. 标记 DOM 容器,建立与 FiberRoot 的关联
markContainerAsRoot(root.current, container);
// 5. 在容器上监听所有支持的事件
listenToAllSupportedEvents(container);
// 6. 返回一个 ReactDOMRoot 实例
return new ReactDOMRoot(root);
}
hydrateRoot
在 React 19 中,hydrateRoot
方法用于将已经存在的 DOM 结构与 React 组件进行“调和”,以确保它们保持同步。这在服务端渲染(SSR)或使用第三方库时非常有用,因为它允许 React 应用在客户端接管已经存在的 DOM 结构。
jsx
import { hydrateRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = hydrateRoot(domNode, <App />);
hydrateRoot
的关键在于它不仅创建了 React 应用的根,还立即启动了一个特殊的“hydration”(水合)渲染过程,旨在重用服务端渲染的 HTML,从而优化首屏加载体验。
在其内部函数中,主要做的事情:
- 接收容器和初始内容:
- 它也接收一个 DOM 元素作为容器,但与
createRoot
不同的是,这个容器期望已经包含了由服务端预先渲染好的 HTML 结构。 - 它还接收一个
initialChildren
参数,这通常是你应用在服务端渲染时使用的顶级 React 元素 (例如<App />
)。
- 它也接收一个 DOM 元素作为容器,但与
- 创建 React 根 (FiberRoot): 这一点与
createRoot
类似,它也会创建FiberRootNode
和HostRootFiber
,为客户端的 React 运行建立基础。 - “激活”服务端 HTML (Hydration): 这是
hydrateRoot
最核心的步骤。- React 不会重新创建 DOM 节点,而是会尝试复用容器中已经存在的、由服务端渲染的 HTML 结构。
- 它会遍历服务端生成的 HTML 和你在客户端提供的
initialChildren
(React 元素树),试图将两者匹配起来。 - 在这个过程中,它会给现有的 DOM 节点附加事件监听器,使得这些静态的 HTML 变得可交互。
- 如果客户端的 React 树结构与服务端的 HTML 结构不完全匹配,React 会尝试进行修复,但可能会在控制台给出警告。严重不匹配可能导致 hydration 失败或行为异常。
- 设置
hydrate
标记: 在内部,它会将FiberRootNode
上的一个hydrate
标志设置为true
,告知后续的渲染流程当前处于 hydration 模式。 - 返回 Root 对象: 与
createRoot
类似,它也会返回一个具备render
(用于后续更新) 和unmount
方法的 Root 对象 (通常是ReactDOMHydrationRoot
实例,其接口与ReactDOMRoot
基本一致)。
简单来说,hydrateRoot
就是告诉 React:“看,这里有一些服务端已经渲染好的 HTML,请你接管它们,让它们在客户端‘活’起来,并且保持与我提供的 React 组件树一致。”
hydrateRoot内部实现
javascript
function hydrateRoot(container, initialChildren, options) {
// 1. 验证容器是否有效
if (!isValidContainer(container)) {
throw new Error('Target container is not a DOM element.');
}
// 2. 初始化选项 (options)
// 3. 创建用于 Hydration 的 FiberRootNode (核心)
const root = createHydrationContainer(
initialChildren, // 初始的 React 元素 (通常是 <App />)
null, // parentComponent (通常为 null)
container, // DOM 容器节点 (期望包含服务端渲染的 HTML)
ConcurrentRoot, // 根节点类型 (并发模式)
hydrationCallbacks, // Hydration 相关的回调 (如 onHydrated, onDeleted)
isStrictMode, // 是否严格模式
false, // concurrentUpdatesByDefaultOverride
identifierPrefix, // 标识符前缀
onUncaughtError, // 未捕获错误处理
onCaughtError, // 已捕获错误处理
onRecoverableError, // 可恢复错误处理
onDefaultTransitionIndicator, // 默认过渡指示器回调
transitionCallbacks,// 过渡回调
formState // 表单状态 (用于服务端组件)
);
// 4. 标记 DOM 容器,建立与 FiberRoot 的关联
markContainerAsRoot(root.current, container);
// 5. 在容器上监听所有支持的事件
listenToAllSupportedEvents(container);
// 6. 返回一个 ReactDOMHydrationRoot 实例
return new ReactDOMHydrationRoot(root);
}
createHydrationContainer
在hydrateRoot
内部中会调用createHydrationContainer
函数,而 createHydrationContainer
函数的主要目标是建立一个配置为水合模式的 FiberRootNode ,并立即调度一个特殊的“水合更新”。这个更新将启动 React 的协调过程,但与常规渲染不同,它会尝试将 initialChildren 与 containerInfo 中已存在的 DOM 结构进行匹配和复用,而不是完全重新创建 DOM。
在 createHydrationContainer
内部函数中,主要的步骤包括:
requestUpdateLane(current)
: 获取一个用于本次更新的优先级“赛道”(Lane)。React 的并发调度模型使用 Lanes 来管理不同优先级的更新。enableHydrationLaneScheduling
: 这是一个特性开关,如果启用,可能会对水合的 Lane 进行特殊调整 (例如,通过 getBumpedLaneForHydrationByLane 提升其优先级或赋予特定属性),以确保水合过程能尽快或以特定方式执行。createUpdate(lane)
: 创建一个更新对象。对于初始水合,这个更新对象通常不携带 payload (即没有新的 element 数据),因为其目的是触发对 initialChildren (已在 FiberRootNode 中设置) 的水合处理,而不是渲染新的内容。update.callback
: 如果用户在调用 hydrateRoot 时提供了回调函数,这个回调会被关联到更新对象上,并在水合操作完成后执行。enqueueUpdate(current, update, lane)
: 将创建的更新对象添加到 HostRoot Fiber 的更新队列中。这使得该更新成为待处理状态。scheduleInitialHydrationOnRoot(root, lane)
: 这是触发实际水合工作调度的关键函数。它会确保 React 的调度器 (Scheduler) 知道有一个水合任务需要在指定的 lane 上执行。这个函数内部会进一步调用调度相关的 API (如 ensureRootIsScheduled ) 来安排工作循环的启动。
createHydrationContainer
内部实现
javascript
// ... existing code ...
export function createHydrationContainer(
initialChildren: ReactNodeList, // 初始的 React 子节点 (期望与服务端渲染的 HTML 结构对应)
callback: ?Function, // 可选的回调函数,在水合完成后执行 (主要用于遗留模式)
containerInfo: Container, // 宿主环境的容器节点 (例如 DOM 元素)
tag: RootTag, // Root 的类型 (通常是 ConcurrentRoot)
hydrationCallbacks: null | SuspenseHydrationCallbacks, // Hydration 相关的回调 (例如 onHydrated)
isStrictMode: boolean, // 是否启用严格模式
concurrentUpdatesByDefaultOverride: null | boolean, // 已被忽略的参数
identifierPrefix: string, // 用于生成唯一 ID 的前缀
onUncaughtError: (error: mixed, errorInfo: {+componentStack?: ?string}) => void, // 未捕获错误的回调
onCaughtError: (error: mixed, errorInfo: {/* ... */}) => void, // 已捕获错误的回调
onRecoverableError: (error: mixed, errorInfo: {/* ... */}) => void, // 可恢复错误的回调
onDefaultTransitionIndicator: () => void | (() => void), // Transition 默认指示器的回调
transitionCallbacks: null | TransitionTracingCallbacks, // Transition 追踪相关的回调
formState: ReactFormState<any, any> | null, // 表单状态
): OpaqueRoot {
// 1. 设置 hydrate 标志为 true
const hydrate = true;
// 2. 调用 createFiberRoot 创建 FiberRootNode
const root = createFiberRoot(
containerInfo,
tag,
hydrate, // <--- 关键:标记为水合模式
initialChildren,
hydrationCallbacks,
isStrictMode,
identifierPrefix,
formState,
onUncaughtError,
onCaughtError,
onRecoverableError,
onDefaultTransitionIndicator,
transitionCallbacks,
);
// 注册 Transition 默认指示器回调
registerDefaultIndicator(onDefaultTransitionIndicator);
// 3. 设置根节点的 context (TODO: 官方注释提到未来可能移到 FiberRoot 构造函数中)
root.context = getContextForSubtree(null);
// 4. 调度初始的水合更新
const current = root.current; // 获取 HostRoot Fiber
let lane = requestUpdateLane(current); // 为此次更新请求一个合适的 Lane
// 如果启用了特定的水合 Lane 调度策略,则调整 Lane
if (enableHydrationLaneScheduling) {
lane = getBumpedLaneForHydrationByLane(lane);
}
// 创建一个更新对象 (Update)
// 注意:这个初始水合的 update 对象没有 payload,它的主要目的是触发调度。
const update = createUpdate(lane);
// 如果提供了回调函数,则将其附加到 update 对象上
update.callback =
callback !== undefined && callback !== null ? callback : null;
// 将更新对象入队到 HostRoot Fiber 的更新队列中
enqueueUpdate(current, update, lane);
// 在根节点上调度初始的水合过程
scheduleInitialHydrationOnRoot(root, lane);
// 5. 返回创建和配置好的 FiberRootNode (OpaqueRoot 类型)
return root;
}
// ... existing code ...
核心函数概念
createFiberRoot
在 createRoot
和 hydrateRoot
内部,都会分别调用核心函数 createContainer
、createHydrationContainer
,它负责创建 FiberRootNode 实例。但在函数内部,都会调用一个核心函数 createFiberRoot
。 在其内部的核心流程如下:
创建
FiberRootNode
实例 (new FiberRootNode(...)
):- 这是整个 React 应用的“大脑”或“中枢系统”。它不是一个 Fiber 节点,而是一个更高级别的对象,用于管理整个 Fiber 树的状态和更新。
- 它存储了指向当前生效的 Fiber 树 (
current
指针)、容器信息 (containerInfo
)、各种配置(如tag
、hydrate
)、错误处理回调 (onUncaughtError
,onCaughtError
,onRecoverableError
)、以及与并发特性相关的上下文(如identifierPrefix
,formState
,transitionCallbacks
)。
创建 HostRoot Fiber 节点:
建立双向链接:
root.current = uninitializedFiber;
:FiberRootNode
的current
属性指向当前活动(或即将成为活动)的 Fiber 树的根 Fiber 节点。在初始化时,这就是我们刚创建的uninitializedFiber
。uninitializedFiber.stateNode = root;
: 相反地,HostRoot Fiber 节点的stateNode
属性指向它所属的FiberRootNode
。这种双向链接使得在遍历 Fiber 树时可以方便地访问到FiberRootNode
,反之亦然。
初始化缓存 (
createCache()
,retainCache()
):- React 引入了 Cache API 用于在 Suspense 等场景下缓存数据。
root.pooledCache
是一个特殊的缓存,用于在渲染过程中为新挂载的组件(特别是 Suspense 边界)提供临时的缓存。它在每次渲染提交后会被清理或转移。
初始化 HostRoot Fiber 的
memoizedState
:memoizedState
存储了 Fiber 节点计算后的状态。- 对于 HostRoot Fiber,其初始
memoizedState
是一个对象,包含:element
: 即initialChildren
,这是 React 应用的初始 JSX 内容,将作为首次渲染的输入。isDehydrated
: 一个布尔值,标记当前根是否处于 “脱水” 状态,这主要用于hydrateRoot
流程,指示 React 需要尝试复用服务端渲染的 HTML。cache
: 指向上面创建的initialCache
。
初始化 HostRoot Fiber 的更新队列 (
initializeUpdateQueue(uninitializedFiber)
):- 更新队列负责存储和处理对该 Fiber 节点状态的更新请求(例如,通过
setState
或forceUpdate
触发的更新)。对于 HostRoot Fiber,初始的render
调用(如root.render(<App />)
)也会通过这个更新队列来调度。
- 更新队列负责存储和处理对该 Fiber 节点状态的更新请求(例如,通过
createFiberRoot
函数是 React 应用启动过程中的奠基石。它构建了应用状态管理的核心 (FiberRootNode
) 和 UI 结构的起点 (HostRoot Fiber),并为后续的渲染和更新流程做好了准备。
createFiberRoot
内部实现
javascript
// ... existing code ...
export function createFiberRoot(
containerInfo: Container, // 宿主环境的容器节点 (例如 DOM 元素)
tag: RootTag, // Root 的类型 (LegacyRoot, ConcurrentRoot)
hydrate: boolean, // 是否是 Hydration 模式
initialChildren: ReactNodeList, // 初始的 React 子节点
hydrationCallbacks: null | SuspenseHydrationCallbacks, // Hydration 相关的回调
isStrictMode: boolean, // 是否启用严格模式
identifierPrefix: string, // 用于生成唯一 ID 的前缀
formState: ReactFormState<any, any> | null, // 表单状态,与 React DOM for form actions 相关
onUncaughtError: (error: mixed, errorInfo: {+componentStack?: ?string}) => void, // 未捕获错误的回调
onCaughtError: (error: mixed, errorInfo: {/* ... */}) => void, // 已捕获错误的回调 (通常由错误边界处理)
onRecoverableError: (error: mixed, errorInfo: {/* ... */}) => void, // 可恢复错误的回调
onDefaultTransitionIndicator: () => void | (() => void), // Transition 默认指示器的回调
transitionCallbacks: null | TransitionTracingCallbacks, // Transition 追踪相关的回调
): FiberRoot {
// 1. 创建 FiberRootNode 实例
const root: FiberRoot = new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
onDefaultTransitionIndicator,
formState,
);
// 如果启用了 Suspense 回调,则设置 hydrationCallbacks
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// 如果启用了 Transition 追踪,则设置 transitionCallbacks
if (enableTransitionTracing) {
root.transitionCallbacks = transitionCallbacks;
}
// 2. 创建 HostRoot Fiber 节点
const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
// 3. 建立 FiberRootNode 和 HostRoot Fiber 之间的双向链接
root.current = uninitializedFiber; // FiberRootNode 的 current 指针指向 HostRoot Fiber
uninitializedFiber.stateNode = root; // HostRoot Fiber 的 stateNode 指向 FiberRootNode
// 4. 初始化缓存 (Cache)
// 用于 Suspense 和其他需要缓存数据的特性
const initialCache = createCache();
retainCache(initialCache); // 增加缓存的引用计数
// pooledCache 用于渲染过程中新挂载的边界,在渲染结束后会被清理或转移
root.pooledCache = initialCache;
retainCache(initialCache);
// 5. 初始化 HostRoot Fiber 的 memoizedState
const initialState: RootState = {
element: initialChildren,
isDehydrated: hydrate, // 标记是否处于 dehydration 状态 (用于 hydrateRoot)
cache: initialCache,
};
uninitializedFiber.memoizedState = initialState;
// 6. 初始化 HostRoot Fiber 的更新队列
initializeUpdateQueue(uninitializedFiber);
// 7. 返回创建好的 FiberRootNode
return root;
}
// ... existing code ...
render
render
方法是 createRoot
或 hydrateRoot
返回的 RootType
对象上的核心 API 之一。它用于告诉 React 需要在根容器中渲染或更新哪些 React元素。
这一步的细节将在协调阶段上进行详细的讲解。
unmount
unmount
方法是 createRoot
或 hydrateRoot
返回的 RootType
对象上的核心 API 之一,调用 root.unmount
以销毁 React 根节点中的一个已经渲染的树。
在其内部主要做了两件事情:
- 调用
updateContainerSync
同步执行,触发实际卸载流程,其会通知Reconciler,让Reconciler
从根节点开始,自顶向下遍历整个 Fiber 树:- 对于每个组件,会执行其卸载相关的生命周期方法(例如,类组件的 componentWillUnmount ,函数组件中 useEffect 的清理函数)。
- 移除所有关联的 DOM 节点。
- 清理事件监听器等资源。
- 调用
flushSyncWork
确保同步工作完成:- 立即执行当前所有挂起的同步优先级的任务。
- 在 unmount 的上下文中,这确保了由
updateContainerSync(null, ...)
触发的卸载操作(包括生命周期方法的执行和 DOM 的移除)被立即、同步地完成,而不是被推迟到未来的某个时间点。
unmount
javascript
// ... existing code ...
ReactDOMHydrationRoot.prototype.unmount = ReactDOMRoot.prototype.unmount =
function (): void {
const root = this._internalRoot; // 获取内部的 FiberRootNode
if (root !== null) { // 检查根是否已经被卸载
this._internalRoot = null; // 将内部根引用置为 null,标记为已卸载
const container = root.containerInfo; // 获取 DOM 容器节点
// 1. 同步地更新容器内容为 null,这将触发组件的卸载生命周期
// updateContainerSync(null, root, null, null);
updateContainerSync(null, root, null, null);
// 2. 确保所有同步工作完成
flushSyncWork();
// 3. 从 DOM 容器上移除 React 根标记
unmarkContainerAsRoot(container);
}
};
// ... existing code ...
unstable_scheduleHydration
unstable_scheduleHydration
是 ReactDOMHydrationRoot
原型上的一个不稳定方法,它实际上是内部函数 scheduleHydration
的别名。这个方法允许你显式地调度一个 DOM
节点的 hydration。
此方法主要用于更细粒度地控制 hydration
过程。通常,当你调用 hydrateRoot
时,React 会尝试 hydration
整个提供的容器。 unstable_scheduleHydration
允许开发者(或 React 内部机制)在初始 hydration
之后,或者对于某些特定情况,指定某个 DOM 节点应该被 hydration。
例如,如果你的页面有一部分内容是SSR的,但另一部分内容(比如一个评论区)是稍后通过客户端请求获取HTML片段并插入到页面中的,你可能希望在插入这个新的HTML片段后,显式地告诉React去 hydrate
这个新片段,而不是重新渲染整个应用或依赖于复杂的父组件状态来触发。
unstable_scheduleHydration
提供了一个底层的、不稳定的接口,用于将特定的 DOM 节点加入队列,以便 React 在后续的渲染周期中尝试对其进行 hydration。这对于处理复杂的、逐步 hydration
的场景可能非常有用。
FiberRoot
在 React 内部,FiberRoot
是一个至关重要的概念。它不是一个 Fiber 节点,而是一个独立的 JavaScript 对象,用于管理整个 React 应用的根节点。每个通过 ReactDOM.render
或 createRoot
挂载的 React 应用都会有一个对应的 FiberRoot
。
FiberRoot
的主要作用:
- 指向根 Fiber 节点:
FiberRoot
的current
属性始终指向当前渲染在屏幕上的HostRootFiber
(即整个 Fiber 树的根节点)。 - 管理更新队列: 它包含了所有待处理的更新(
pendingLanes
),以及与调度相关的优先级信息。 - 存储工作中的 Fiber 树: 在协调阶段,
FiberRoot
会有一个指向正在构建的workInProgress
树的引用。 - 错误边界: 它是错误边界捕获错误的起点。
- 上下文: 存储一些全局上下文信息。
HostRootFiber
是 FiberRoot
所指向的那个特殊的 Fiber 节点。它的 stateNode
属性指向真实的 DOM 元素,例如 <div id="root"></div>
。HostRootFiber
是整个 React 组件树的入口,所有其他组件的 Fiber 节点都是它的子孙。
它们之间的关系可以概括为:
FiberRoot
是一个管理对象,它“拥有”一个 HostRootFiber
。HostRootFiber
则是 Fiber 树的实际根节点,它代表了 React 应用在真实 DOM 中的挂载点。
流程图
为了让大家能更快理解整个流程,我将整个入口的流程图给大家展示一下,
