通俗易懂的React原理(十一):从createRoot说起

代码以React v19.1.0为例,https://github.com/facebook/react/tree/v19.1.0 ,并且经过简化,仅保留了关键的逻辑,以便于理解流程

前面我们一直在讲React渲染的流程,但是我们并不是从我们最初的调用开始讲的。回顾前面的十篇文章,我们是先讲了React渲染的流程,先跳过了触发渲染这一步。然后讲了一个setState或者说是dispatch的行为是怎么触发渲染的,然后又讲了Scheduler在其中起到了什么作用。

而现在,我们还没有说到最初,比如说浏览器刚拿到js资源的时候,他是怎么从零开始,把React应用渲染成页面的。那么今天,我们就来看一看。

createRoot

React18开始,react-dom有了createRoot这个API。在React17及以前,我们通常是这样写:ReactDom.render(<App />, document.getElementById('root'))。而现在有了新的API,React更推荐这样写:createRoot(document.getElementById('root')).render(<APP />)

通过新写法来生成的React应用,才可以使用并发特性。新写法分成了两部,先是从一个dom节点,生成一个root,然后调用root.render方法,把React应用渲染出来。

我们一点一点看这其中都发生了什么。

这部分代码在packages\react-dom\src\client\ReactDOMRoot.js

1
2
3
4
5
6
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType{
// 略
}

对于createRoot方法,有两个参数,第一个参数就是真实的dom节点,我们会把React应用挂到这个dom下面。第二个参数我们通常不会传,不过它里面有些有用的字段比如onUncaughtErroronCaughtErroronRecoverableError之类的,可以用来捕获并上报异常(对于水合时候用的hydrateRoot更重要,可以捕获水合错误,不然你就猜是哪里水合匹配不上吧),详见React文档。

createRoot的核心逻辑如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
transitionCallbacks,
);
markContainerAsRoot(root.current, container);

const rootContainerElement: Document | Element | DocumentFragment =
!disableCommentsAsDOMContainers && 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);
}

function ReactDOMRoot(internalRoot: FiberRoot) {
this._internalRoot = internalRoot;
}

首先,通过createContainer方法,得到FiberRootNode, 就是上面的变量root

然后markContainerAsRoot方法,其实就是在dom节点上面,用一个字段,把FiberRootNode保存起来,便于直接从dom节点获取到FiberRootNode

接下来,通过listenToAllSupportedEvents方法完成事件委托,将监听事件都绑定到传入的dom上,这一环节我们会在后续文章里专门探讨。

接下来,new一个ReactDOMRoot实例,将root传入,挂在_internalRoot字段上, 将ReactDOMRoot实例return回去。

我们先看一下,createContainer方法干了什么

createContainer与createFiberRoot

以下代码在packages\react-reconciler\src\ReactFiberReconciler.jspackages\react-reconciler\src\ReactFiberRoot.js

createContainer可以认为直接调用了createFiberRoot,所以我们看createFiberRoot就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
export function createFiberRoot(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
initialChildren: ReactNodeList,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
isStrictMode: boolean,
// TODO: We have several of these arguments that are conceptually part of the
// host config, but because they are passed in at runtime, we have to thread
// them through the root constructor. Perhaps we should put them all into a
// single type, like a DynamicHostConfig that is defined by the renderer.
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,
transitionCallbacks: null | TransitionTracingCallbacks,
formState: ReactFormState<any, any> | null,
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onUncaughtError,
onCaughtError,
onRecoverableError,
formState,
): any);

const uninitializedFiber = createHostRootFiber(tag, isStrictMode);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;

const initialState: RootState = {
element: initialChildren,
isDehydrated: hydrate,
cache: initialCache,
};
uninitializedFiber.memoizedState = initialState;

initializeUpdateQueue(uninitializedFiber);

return root;
}

将root赋值为一个FiberRootNode的实例,这个FiberRootNode函数里面的逻辑就很简单了,只是单纯的给实例挂了很多很多字段,没有其他逻辑。

接下来,调用createHostRootFiber,得到一个HostRootFiber, 也就是变量uninitializedFiber。生成它的逻辑也不复杂,就是调用createFiber, 生成一个Fiber的实例,差不多也就是new一个对象,我们略过。

然后,将FiberRootNodeHostRootFiber互相绑定起来,FiberRootNode的current指向HostRootFiber,而HostRootFiber的stateNode指向FiberRootNode。当然最开始,我们讲到Fiber树的双缓冲实现,是在两棵树之间切换的,指的就是FiberRootNode的current指针,可能会指向另一颗Fiber树。

然后,将HostRootFibermemoizedState字段,赋值为一个初始的State,然后执行initializeUpdateQueue方法,将queue也挂到HostRootFiber上面,如下面所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export function initializeUpdateQueue<State>(fiber: Fiber): void {
const queue: UpdateQueue<State> = {
baseState: fiber.memoizedState,
firstBaseUpdate: null,
lastBaseUpdate: null,
shared: {
pending: null,
lanes: NoLanes,
hiddenCallbacks: null,
},
callbacks: null,
};
fiber.updateQueue = queue;
}

这里我们注意到,之前我们介绍hooks的时候,也提到过memoizedState啊,queue之类的,而且他们很像。其实是这样的,只不过hooks是以双向链表的形式挂在函数组件Fiber的memoizedState上的,然后每个hooks对应的更新放在对应hooks的queue里,这是函数组件。

而HostRootFiber节点和类组件,则是将state和更新,直接挂在了Fiber.memoizedStateFiber.queue上。

那我们回过头去看createFiberRoot方法,其实他已经得到了一个FiberRootNode,返回给了createRoot方法。

createRoot().render

接下来,我们就该看这个render方法了。

1
2
3
4
5
6
7
8
9
10
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.');
}

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

前面我们知道,createRoot方法,返回的是一个ReactDOMRoot的实例,真正的FiberRootNode是挂在这个实例的_internalRoot属性上的。

而我们调用的render方法,是实例的方法,所以定义在了构造函数的prototype上。

updateContainer

核心的逻辑在updateContainer里面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
const current = container.current;
const lane = requestUpdateLane(current);
updateContainerImpl(
current,
lane,
element,
container,
parentComponent,
callback,
);
return lane;
}

我们可以从前文看到,render方法传过来的containerFiberRootNode,那么container.current就是HostRootFiber了。

然后获取lane。如果你没有用transition包裹createRoot方法,即通常情况下,优先级最终可以追溯到下面这个方法里。由于此时window.event是undefined,所以会进入到默认优先级DefaultLane。这是一个略低于SyncLane,以及其他交互相关的优先级,但是它比transition的优先级要高。(注意,优先级的绝对值没有什么意义,因为他们在不同版本可能会变化。它们之间的大小关系是更重要的)

1
2
3
4
5
6
7
8
9
10
11
export function resolveUpdatePriority(): EventPriority {
const updatePriority = ReactDOMSharedInternals.p; /* currentUpdatePriority */
if (updatePriority !== NoEventPriority) {
return updatePriority;
}
const currentEvent = window.event;
if (currentEvent === undefined) {
return DefaultEventPriority;
}
return getEventPriority(currentEvent.type);
}

接下来,调用包了一层的方法,updateContainerImpl

updateContainerImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function updateContainerImpl(
rootFiber: Fiber,
lane: Lane,
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): void {
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}

const update = createUpdate(lane);
// Caution: React DevTools currently depends on this property
// being called "element".
update.payload = {element};

callback = callback === undefined ? null : callback;
if (callback !== null) {
update.callback = callback;
}

const root = enqueueUpdate(rootFiber, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, rootFiber, lane);
entangleTransitions(root, rootFiber, lane);
}
}

记得当前调用的时候,后两个参数都是null。

创建一个update字面量对象,将render方法的参数,也就是element赋值给update.payload。然后,执行enqueueUpdate方法,将这个update放到FiberRootNode的更新队列里。等到后面开始渲染,遍历Fiber树的时候,会在beginWork里面完成对FiberRootNode的更新.

接下来,调用scheduleUpdateOnFiber方法,让调度器安排一个渲染,这个方法我们前面已经介绍过了。

entangleTransitions方法,则只在当前是transition优先级的时候才有实际作用,我们前面提过了,没有用transition包裹的createRootDefaultLane

以上,由于调用了scheduleUpdateOnFiber,React将会开始渲染整颗Fiber树,然后你的React应用就呈现在页面上了。

总结

首先,createRoot方法,生成了FiberRootNodeHostRootFiber两个变量,他们两个的关系如下:

1
2
FiberRootNode.current = HostRootFiber
HostRootFiber.stateNode = FiberRootNode

其中,FiberRootNode可以理解为React应用的最高管理者,它的current指针指向HostRootFiber,并且可以切换到HostRootFiber.alternate上,以实现前后缓冲区的切换。

然后,执行render方法的时候,给HostRootFiber添加一个update,update.payload就是render方法传入的Fiber。然后调用scheduleUpdateOnFiber,触发一次React的渲染,就完成了初次渲染。


通俗易懂的React原理(十一):从createRoot说起
https://miku03090831.github.io/2025/10/22/通俗易懂的React原理(十一):从createRoot说起/
作者
qh_meng
发布于
2025年10月22日
许可协议