「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。
react 版本:v17.0.3
在《React 源码解读之React应用的2种启动方式》中我们有介绍到:React 启动应用有 legacy 和 Concurrent 两种模式,其中 legacy 模式调用 ReactDOM.render(<App />, rootNode)
启动 React app 应用,接下来我们看看当执行ReactDOM.render()时,React做了什么事情。
render
// 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 函数通常是React app 应用的入口函数,其中element参数是要渲染的 React Element 元素。container 参数是容器,即要渲染的 React Element 元素要挂靠在页面上的哪个根DOM容器下。callback 参数是可选的,在初次渲染或者更新完成后,如果定义了 callback 回调函数,则会执行callback。
在执行ReactDOM.render() 时,会做两件事情:创建fiber和创建update。下面我们分别来看看这两个过程。
创建fiber
在首次执行ReactDOM.render()时,会创建ReactDOMRoot、FiberRoot、HostRootFiber 3个全局对象。其中 ReactDOMRoot 是整个应用的根节点,FiberRoot 是要渲染的组件所在组件树的根节点。
ReactDOMRoot 对象是在legacyCreateRootFromDOMContainer() 函数中创建的,legacyCreateRootFromDOMContainer() 函数则是在 legacyRenderSubtreeIntoContainer 函数中被调用的,我们先来看看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 应用环境。为了快速完成渲染,初次调用ReactDOM.render时的更新是非批量更新。
// 创建 ReactDOMRoot 对象,初始化 React 应用环境 // 创建的 ReactDOMRoot 对象会存储在 container 的 _reactRootContainer 属性上 root = container._reactRootContainer = legacyCreateRootFromDOMContainer( container, // 参数 container 即为 document.getElementById('root') forceHydrate, );
-
如果是非初次调用ReactDOM.render,则获取存储在 container 上的 ReactDOMRoot 对象,然后更新 container 容器。
// 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);
-
最后通过 getPublicRootInstance 将 reactElement() 和 DOM对象 div#root 关联起来。
// instance最终指向 children(入参: 如<App/>)生成的dom节点 return getPublicRootInstance(fiberRoot);
接下来看看 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);
};
在 createFiber 中,返回了 FiberNode 的一个实例对象,即返回一个新的fiber节点。
创建update
无论是首次执行 ReactDOM.render 还是多次调用 ReactDOM.render,都会调用 updateContainer函数创建 update 来开启一次更新。
// react-reconciler/src/ReactFiberReconciler.new.js
export function updateContainer(
element: ReactNodeList,
container: OpaqueRoot,
parentComponent: ?React$Component<any, any>,
callback: ?Function,
): Lane {
// ...
// container 的current属性保存了更新过程中的一棵fiber树,对应着屏幕上已经渲染好的内容
// 获取更新过程中的 current树
const current = container.current;
// 获取当前时间,通过 performance.now() 或 Date.now() 获取的秒数
// 其源码在 scheduler/src/forks/Scheduler.js 中的 getCurrentTime 函数中
const eventTime = requestEventTime();
// 创建一个优先级变量(lane模型,通常称为车道模型)
const lane = requestUpdateLane(current);
if (enableSchedulingProfiler) {
markRenderScheduled(lane);
}
// 获取 context
const context = getContextForSubtree(parentComponent);
if (container.context === null) {
container.context = context;
} else {
container.pendingContext = context;
}
// ...
// 新建一个 update
const update = createUpdate(eventTime, lane);
// Caution: React DevTools currently depends on this property
// being called "element".
// update.payload 为需要挂载造根节点的组件
update.payload = { element };
// 这里的callback是 legacy启动模式 ReactDOM.render(<App />, document.getElementById('root'), dom => {}); 的 回调函数
// Concurrent模式 :ReactDOM.createRoot(rootNode).render(<App />) 不支持回调
callback = callback === undefined ? null : callback;
if (callback !== null) {
// ...
update.callback = callback;
}
// 将新建的 update 添加到 update链表中
enqueueUpdate(current, update, lane);
// 进入任务调度
const root = scheduleUpdateOnFiber(current, lane, eventTime);
if (root !== null) {
entangleTransitions(root, current, lane);
}
return lane;
}
在 updateContainer 中,调用 createUpdate 创建一个新的 update 后,将该 update 添加到 updateQueue 中(updateQueue是一个环形链表结构),然后通过调用 scheduleUpdateOnFiber 函数,进入任务调度流程。详情请阅读《React 源码解读之任务调度流程》
流程图
ReactDOM.render的执行过程,是从 react-dom 包发起,内部调用了 react-reconciler 包,其流程如下:
总结
在执行ReactDOM.render() 时,主要做了两件事情:创建fiber和创建update。
如果是首次执行ReactDOM.render,则会创建三个对象,分别是ReactDOMRoot对象、FiberRoot对象、HostRootFiber对象。其中 ReactDOMRoot 是整个应用的根节点,在 legacyCreateRootFromDOMContainer 函数中创建,并在该函数中完成了事件的监听。FiberRoot 是要渲染组件所在组件树的根节点,在 createFiberRoot 函数中创建,作为 react-reconciler 在运行过程中的全局上下文,保存着fiber构建过程中所依赖的全局状态。
在首次执行ReactDOM.render创建完三个对象后,调用 updateContainer 创建update并添加到updateQueue中,然后进入任务调度流程。
如果是非首次执行ReactDOM.render,则直接获取存储在 container 上的 ReactDOMRoot 对象,然后也是一样调用 updateContainer 创建update并添加到updateQueue中,并进入任务调度流程。