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对象
ReactDOM.createBlockingRoot(rootNode).render(<App />)。目前正在实验中, 它仅提供了 concurrent 模式的小部分功能,作为迁移到 concurrent 模式的第一个步骤。
// BolckingRoot
// 1. 创建ReactDOMRoot对象
const reactDOMBolckingRoot = ReactDOM.createBlockingRoot(
document.getElementById('root'),
);
// 2. 调用render
reactDOMBolckingRoot.render(<App />); // 不支持回调
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 中,主要做了三件事:
-
调用 createContainer 函数创建 ReactDOMRoot对象。
-
将根DOM容器 container(div#root) 标记为 hostRoot,即标记为根节点。
-
在根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件事:
-
调用 new FiberRootNode() 创建根DOM容器div#root的fiber根节点。
-
调用 createHostRootFiber 函数创建react应用的首个 fiber 对象,其称为 HostRootFiber。
-
将根DOM容器的fiber节点与 HostRootFiber 关联起来。
-
初始化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个全局对象。
- ReactDOMRoot 对象
该对象属于 react-dom 包,在 legacyCreateRootFromDOMContainer 函数中创建,并在该函数中完成了事件的监听。
- FiberRoot 对象
该对象属于 react-reconciler 包,在 createFiberRoot 函数中创建,作为 react-reconciler 在运行过程中的全局上下文,保存着fiber构建过程中所依赖的全局状态。
- 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 函数中,主要做了四件事情:
-
调用 react-reconciler 包的 createContainer 创建 FiberRoot 对象,该 FiberRoot 对象将会被挂载到 ReactDOMRoot 对象的 _internalRoot 属性上。在创建 FiberRoot 对象的过程中,还会创建 HostRootFiber 对象。
-
将根容器DOM(div#root) 标记为 hostRoot,即标记为根节点。
-
在根DOM容器(div#root)上监听事件。
-
调用 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 对象关联在一起。