ReactDOM.createRoot
创建项目并打断点
首先先用 React 的 CLI(Create-React-App)创建一个初始项目。
然后尝试在入口文件: src/main.jsx 打debugger
断点:
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
debugger;
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
运行,然后就可以开始调试啦。
createRoot
接下来一步步点击 step into next function call,我们可以看到首先被调用的是 ReactDOM.createRoot()
。
进入到这个方法,我们可以看到如下的代码:
这段代码根据环境的不同导出不同的 createRoot
函数和 hydrateRoot
函数。
createRoot 为受支持的 container 创建一个 React root。这个 root 可以用 render 方法将一个 React element 渲染到 DOM 中。
hydrateRoot则和 createRoot 类似,除了它是用来给 由 ReactDOMServer 渲染 HTML 内容的 container 添加额外内容的(hydrate the container,我不知道咋翻译,暂时先这样理解)。React 会尝试给存在的标记(markup)添加事件监听器。
让我们尝试在源码中找到这个函数。我们先根据 CRA 中的import ReactDOM from "react-dom/client";
找到 packages/react-dom/client.js 中的 createRoot
:
import type {
RootType,
HydrateRootOptions,
CreateRootOptions,
} from "./src/client/ReactDOMRoot";
import {
createRoot as createRootImpl,
hydrateRoot as hydrateRootImpl,
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED as Internals,
} from "./";
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType {
if (__DEV__) {
Internals.usingClientEntryPoint = true;
}
try {
return createRootImpl(container, options);
} finally {
if (__DEV__) {
Internals.usingClientEntryPoint = false;
}
}
}
我们可以看到 client 中createRoot
的外层逻辑。
它接受两个参数:
- container: 必填参数,是用于创建 React root 所在的 DOM container。我们可以看到受支持的 container 包含三个类型,Element、Document 和 DocumentFragment。
- options: 可选参数,是 createRoot 的一些配置项。
如果是 development 模式,将 Internals.usingClientEntryPoint
变为 true
,标识当前是从 react-dom/clint 中正确引入了 createRoot
,这个后面会用到。然后返回 createRootImpl(container, options)
,最后恢复 Internals.usingClientEntryPoint
回 false
。
然后根据 createRootImpl
找到 packages/react-dom/index.js,该文件导出的 createRoot
来自 packages/react-dom/src/client/ReactDOM.js,相关部分代码如下:
import {
createRoot as createRootImpl,
hydrateRoot as hydrateRootImpl,
isValidContainer,
} from "./ReactDOMRoot";
// ...
function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType {
if (__DEV__) {
if (!Internals.usingClientEntryPoint && !__UMD__) {
console.error(
'You are importing createRoot from "react-dom" which is not supported. ' +
'You should instead import it from "react-dom/client".'
);
}
}
return createRootImpl(container, options);
}
// ...
我们看到函数中的这个 if (__DEV__)
块, 如果是开发模式,且 Internals.usingClientEntryPoint
为 false
(暂时先不管__UMD__
),就报错提示:从 react-dom 引入 createRoot 是不支持的,应该从 react-dom/client 引入。而生产模式就会返回 createRootImpl。所以前面在 packages/react-dom/client.js 中,将Internals.usingClientEntryPoint
置为 true
就是为了实现这个目的。不过目前为止似乎看不出分别从 packages/react-dom/client.js 和 packages/react-dom/index.js 中引入 createRoot
有什么太大的区别,让我们继续看下去。
然后让我们来到 packages/react-dom/src/client/ReactDOMRoot.js 中的 createRoot
,这一段的逻辑相对来说就比较多,让我们一块块看。
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType {
if (!isValidContainer(container)) {
throw new Error("createRoot(...): Target container is not a DOM element.");
}
warnIfReactDOMContainerInDEV(container);
//省略后面,我们先看前两块
// ...
}
首先是 if (!isValidContainer(container)){}
,这个就是判断传入的是否是一个有效的 container,暂时不是很感兴趣,所以先忽略这一块。
然后让我们看看 warnIfReactDOMContainerInDEV(container)
:
function warnIfReactDOMContainerInDEV(container: any) {
if (__DEV__) {
if (
container.nodeType === ELEMENT_NODE &&
((container: any): Element).tagName &&
((container: any): Element).tagName.toUpperCase() === "BODY"
) {
console.error(
"createRoot(): Creating roots directly with document.body is " +
"discouraged, since its children are often manipulated by third-party " +
"scripts and browser extensions. This may lead to subtle " +
"reconciliation issues. Try using a container element created " +
"for your app."
);
}
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."
);
}
}
}
}
这一段主要就是在 DEV 模式的时候,三种和 container 相关的警告。
-
当传入的 container 是
<body/>
的时候index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite + React</title> </head> <body id="root"> <script type="module" src="/src/main.jsx"></script> </body> </html>
/src/main.jsx
import React from "react"; import ReactDOM from "react-dom/client"; import App from "./App"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")).render( <React.StrictMode> <App /> </React.StrictMode> );
-
给
ReactDOMClient.createRoot()
传入了一个之前已经传给ReactDOM.render()
的 container。 -
给
ReactDOMClient.createRoot()
传入了一个之前已经传给createRoot()
的 container。
接下来这一段就是 options 参数配置相关的内容:
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType {
//...
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = "";
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 (
allowConcurrentByDefault &&
options.unstable_concurrentUpdatesByDefault === true
) {
concurrentUpdatesByDefaultOverride = true;
}
if (options.identifierPrefix !== undefined) {
identifierPrefix = options.identifierPrefix;
}
if (options.onRecoverableError !== undefined) {
onRecoverableError = options.onRecoverableError;
}
if (options.unstable_transitionCallbacks !== undefined) {
transitionCallbacks = options.unstable_transitionCallbacks;
}
}
//...
}
这一段单独看感觉没有太大意义,需要结合后面具体场景再理解学习。
那么让我们看到这个方法的最后一段:
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions
): RootType {
//...
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks
);
markContainerAsRoot(root.current, container);
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
listenToAllSupportedEvents(rootContainerElement);
return new ReactDOMRoot(root);
}
首先看到这个 createContainer
,它来自于 react-reconciler/src/ReactFiberReconciler.js 中。这一部分就是和 reconciler 相关的内容了:
import { enableNewReconciler } from "shared/ReactFeatureFlags";
import {
createContainer as createContainer_old,
// ...
} from "./ReactFiberReconciler.old";
import {
createContainer as createContainer_new,
// ...
} from "./ReactFiberReconciler.new";
export const createContainer = enableNewReconciler
? createContainer_new
: createContainer_old;
但我们可以看到,react-reconciler/src/ReactFiberReconciler.js 中通过 enableNewReconciler
区分出了 new Reconciler 和 old Reconciler。在 shared/ReactFeatureFlags.js 中,enableNewReconciler
为 false,然后我发现全局搜索 enableNewReconciler
时,发现在 packages/shared/forks/ReactFeatureFlags.www.js 存在不一样的内容:
// ...
// Enable forked reconciler. Piggy-backing on the "variant" global so that we
// don't have to add another test dimension. The build system will compile this
// to the correct value.
export const enableNewReconciler = __VARIANT__;
//...
而这个__VARIANT__
,我们可以在 scripts/rollup/build.js 找到:
// ...
return [
// ...,
replace({
__DEV__: isProduction ? "false" : "true",
__PROFILE__: isProfiling || !isProduction ? "true" : "false",
__UMD__: isUMDBundle ? "true" : "false",
"process.env.NODE_ENV": isProduction ? "'production'" : "'development'",
__EXPERIMENTAL__,
// Enable forked reconciler.
// NOTE: I did not put much thought into how to configure this.
__VARIANT__: bundle.enableNewReconciler === true,
}),
// ...
];
// ...
我理解是在 rollup 的构建过程中,会根据不同 bundle 的配置使用不同的 reconciler。我一直对 rollup 的构建很感兴趣,所以我准备先大致过一遍 在 React 中是如何使用 rollup 构建的,主要是搞清楚什么情况下会启用新的 reconciler。
先暂停这篇,学习 rollup 构建的记录另起一篇
衍生知识点
1. flow 的 explicit inexact object types/显示不确定对象类型
我们看 createRoot 接受的第二个参数 options 的类型定义:
export type CreateRootOptions = {
unstable_strictMode?: boolean,
unstable_concurrentUpdatesByDefault?: boolean,
unstable_transitionCallbacks?: TransitionTracingCallbacks,
identifierPrefix?: string,
onRecoverableError?: (error: mixed) => void,
...
};
我们可以看到对象内部最后有一个...
,它标识着:除了已经定义的几个属性的类型,还可以自由传入其他任意类型的任意属性。
默认状态下,对象的每个属性以及对应的类型都是确定的。所以当我们需要使用不确定的对象类型时,需要显示的去标识。