React18.2.0源码解析2:ReactDom核心API
ReactDom包是针对web平台的render
包,主要分为客户端(client)API
和服务端(server)API
。
客户端(client)API
客户端API主要在packages/src/client
目录下,一般我们使用时是import ReactDOM from 'react-dom/client'
,通过查看npm
目录下存在一个client.js
,即导出代码是使用的此文件代码。
packages/react-dom/npm/client.js
'use strict';
var m = require('react-dom');
if (process.env.NODE_ENV === 'production') {
exports.createRoot = m.createRoot;
exports.hydrateRoot = m.hydrateRoot;
} else {
...
}
由上述源码可知,其再次在文件中引入react-dom
包,并且只导出了createRoot
、hydrateRoot
。在packages.json
中可查看main: 'index.js'
,即:var m = require('react-dom');
导出的是packages/react-dom/index.js
中的内容
packages/react-dom/index.js
export {
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
createPortal,
createRoot,
hydrateRoot,
findDOMNode,
flushSync,
hydrate,
render,
unmountComponentAtNode,
unstable_batchedUpdates,
unstable_createEventHandle,
unstable_flushControlled,
unstable_isNewReconciler,
unstable_renderSubtreeIntoContainer,
unstable_runWithPriority, // DO NOT USE: Temporarily exposed to migrate off of Scheduler.runWithPriority.
version,
} from './src/client/ReactDOM';
由上述源码可知,packages/react-dom/index.js
又导出了packages/react-dom/src/client/ReactDOM.js
的内容,查看此文件发现其中都是实现没有再次导入导出。则最终import ReactDOM from 'react-dom/client'
导出的API如下:
export {
/**
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
*/
createPortal,
createRoot,
/**
hydrateRoot,
findDOMNode,
flushSync,
hydrate,
render,
unmountComponentAtNode,
unstable_batchedUpdates,
unstable_createEventHandle,
unstable_flushControlled,
unstable_isNewReconciler,
unstable_renderSubtreeIntoContainer,
unstable_runWithPriority,
version,
*/
} from './src/client/ReactDOM';
其中注释掉的为不重要的API
createPortal
允许将子元素渲染到制定DOM中
<div>
<p>段落1挂载父元素上</p>
{createPortal(
<p>段落2挂载到body上</p>,
document.body
)}
</div>
源码
/**
* packages/react-dom/src/client/ReactDOM.js
*/
function createPortal(
children: ReactNodeList,
container: Element | DocumentFragment,
key: ?string = null,
): React$Portal {
if (!isValidContainer(container)) {
throw new Error('Target container is not a DOM element.');
}
return createPortalImpl(children, container, null, key);
}
/**
* packages/react-reconciler/src/ReactPortal.js
*/
export function createPortal(
children: ReactNodeList,
containerInfo: any,
implementation: any,
key: ?string = null,
): ReactPortal {
if (__DEV__) {
checkKeyStringCoercion(key);
}
return {
$$typeof: REACT_PORTAL_TYPE,
key: key == null ? null : '' + key,
children,
containerInfo,
implementation,
};
}
由源码可知,其实际上封装了reconciler
中的createPortal
方法,最终创建了一个REACT_PORTAL_TYPE
类型的对象
createRoot
创建一个root,用于在浏览器DOM中显示React组件
const root = ReactDOM.createRoot(document.querySelector('#root'))
root.render(<App />)
源码
/**
* packages/react-dom/src/client/ReactDOM.js
*/
function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
if (__DEV__) {
...
}
return createRootImpl(container, options);
}
/**
* packages/react-dom/src/client/ReactDOMRoot.js
*/
export function createRoot(
container: Element | Document | DocumentFragment,
options?: CreateRootOptions,
): RootType {
if (!isValidContainer(container)) {
throw new Error('createRoot(...): Target container is not a DOM element.');
}
// 开发环境下的错误验证:
// `createRoot接收的不能是body元素`
// `不能对同一个元素进行createRoot和ReactDOM.render`
// `不能对同一个元素进行两次createRoot`
warnIfReactDOMContainerInDEV(container);
let isStrictMode = false;
let concurrentUpdatesByDefaultOverride = false;
let identifierPrefix = '';
let onRecoverableError = defaultOnRecoverableError;
let transitionCallbacks = null;
// 处理options
if (options !== null && options !== undefined) {
if (__DEV__) {
...
}
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.transitionCallbacks !== undefined) {
transitionCallbacks = options.transitionCallbacks;
}
}
// 创建fiber root
const root = createContainer(
container,
ConcurrentRoot,
null,
isStrictMode,
concurrentUpdatesByDefaultOverride,
identifierPrefix,
onRecoverableError,
transitionCallbacks,
);
// 将fiber root绑定到元素属性['__reactContainer$' + randomKey]上
markContainerAsRoot(root.current, container);
const rootContainerElement: Document | Element | DocumentFragment =
container.nodeType === COMMENT_NODE
? (container.parentNode: any)
: container;
// 监听所以支持的事件
listenToAllSupportedEvents(rootContainerElement);
// 返回ReactRoot实例
return new ReactDOMRoot(root);
}
其中,核心的是方法为createContainer
,但此方法最终是调用packages/react-reconciler/src/ReactFiberRoot.js
的createFiberRoot
方法,后面再说。其中在ReactDOMRoot
类上还挂在了两个方法:render
、unmount
render
function (children: ReactNodeList): void {
const root = this._internalRoot;
if (root === null) {
throw new Error('Cannot update an unmounted root.');
}
if (__DEV__) {
...
}
updateContainer(children, root, null, null);
};
此方法最终是调用的react-reconciler
中的updateContainer
unmount
// packages/react-dom/src/client/ReactDOM.js
function (): void {
if (__DEV__) {
...
}
const root = this._internalRoot;
if (root !== null) {
this._internalRoot = null;
const container = root.containerInfo;
if (__DEV__) {
if (isAlreadyRendering()) {
console.error(
'Attempted to synchronously unmount a root while React was already ' +
'rendering. React cannot finish unmounting the root until the ' +
'current render has completed, which may lead to a race condition.',
);
}
}
flushSync(() => {
updateContainer(null, root, null, null);
});
unmarkContainerAsRoot(container);
}
};
// packages/react-dom/src/client/ReactDOMComponentTree.js
export function unmarkContainerAsRoot(node: Container): void {
node[internalContainerInstanceKey] = null;
}
unmount
方法也是调用的updateContainer
,将其所有属性只为null
通过react
、react-dom
两个包来看,其实际代码都在react-reconciler
包中