共计 4458 个字符,预计需要花费 12 分钟才能阅读完成。
初步看了 react-dom 这个包的一些源码,发现其比 react 包要复杂得多,react 包中基本不存在跨包调用的情况,他所做的也仅仅是定义了 ReactElement 对象,封装了对 ReactElement 的基本操作,而 react-dom 包存在复杂的函数调用。本文将对 ReactDOM.render 源码做一个初步解析。文章中如有不当之处,欢迎交流指点。react 版本 16.8.2。在源码添加的注释在 githubreact-source-learn 的 learn 分支。
前言
使用 react 时常常写类似下面的代码:
import ReactDOM from ‘react-dom’;
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById(‘root’)
);
代码 1
这里导入的 ReactDOM 就是 packages/react-dom/client/ReactDOM.js 中所导出的对象。从文档可见 ReactDOM 对象有如下几个方法:(ps: 从源码看其实还有很多其他方法)
render()
hydrate()
unmountComponentAtNode()
findDOMNode()
createPortal()
本文只介绍 render() 方法
代码分析
render 方法定义如下:
render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function,
) {
invariant(
// 1
isValidContainer(container),
‘Target container is not a DOM element.’,
);
if (__DEV__) {
warningWithoutStack(
!container._reactHasBeenPassedToCreateRootDEV,
‘You are calling ReactDOM.render() on a container that was previously ‘ +
‘passed to ReactDOM.%s(). This is not supported. ‘ +
‘Did you mean to call root.render(element)?’,
enableStableConcurrentModeAPIs ? ‘createRoot’ : ‘unstable_createRoot’,
);
}
// 2
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
},
代码 2
render 方法接收两个必选参数可一个可选参数,结合代码 1 的调用可知,element 是一个 ReactElement 对象,container 是一个 dom 节点,callback 在上面的代码 1 并没有指定,他是一个可选函数。
这个 render 方法做的事情比较简单,一是校验 container 参数,二是调用 legacyRenderSubtreeIntoContainer 方法并返回。
接下来是 legacyRenderSubtreeIntoContainer
// 删除了第一次调 ReactDOM.render 不会走的分支
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>, // ReactDOM.render 是 null
children: ReactNodeList, // 是一个 ReactElement , ReactDOM.render 是第一个参数
container: DOMContainer, // 是一个 dom 节点, ReactDOM.render 是第二个参数
forceHydrate: boolean, // ReactDOM.render 是 false
callback: ?Function, // ReactDOM.render 是 第三个参数
) {
if (__DEV__) {
topLevelUpdateWarnings(container);
}
// TODO: Without `any` type, Flow says “Property cannot be accessed on any
// member of intersection type.” Whyyyyyy.
// 根据 type 知道, Root type 是个对象, 包含
// render 方法
// unmount 方法
// legacy_renderSubtreeIntoContainer 方法
// createBatch 方法
// _internalRoot 属性
let root: Root = (container._reactRootContainer: any);
if (!root) {// ReactDOM.render 调用时走这里
// Initial mount
// 调用 legacyCreateRootFromDOMContainer 拿 Root
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate, // ReactDOM.render 是 false
);
// 在 callback 加参数
if (typeof callback === ‘function’) {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(root._internalRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
// 这个是 packages/react-reconciler/ReactFiberScheduler.js 中的方法
// TOLEARN: 这个里边应该是一些调度过程, 后续再看
unbatchedUpdates(() => {
if (parentComponent != null) {
root.legacy_renderSubtreeIntoContainer(
parentComponent,
children,
callback,
);
} else {// ReactDOM.render 方法走这里
// 这里的 root.render 返回的是一个叫 Work 的东西, TOLEARN, 这个 Work 后面再做了解
root.render(children, callback);
}
});
}
// getPublicRootInstance 是 /packages/react-reconciler/ReactFiberReconciler.js 中的方法
// 关于他是返回的一个什么东西, 后面再看, 总之他是我 ReactDOM.render 方法回调函数的一个参数,
// 也是返回值
return getPublicRootInstance(root._internalRoot);
}
legacyRenderSubtreeIntoContainer 在第一次 render 时做了如下事情:
调用 legacyCreateRootFromDOMContainer 拿到一个 ReactRoot 的实例
在 callback 中注入一个参数 instance
调用 unbatchedUpdates 开始一轮调度过程,这个是猜的
返回 instance
关于 instance 的获取是调用的 getPublicRootInstance,getPublicRootInstance 是 /packages/react-reconciler/ReactFiberReconciler.js 中的方法,后面再研究
关于 unbatchedUpdates, 这个东西是这个是 packages/react-reconciler/ReactFiberScheduler.js 中的方法,简单看了看还没太明白
接下来看一下 ReactRoot 实例的获取
// 删除了__DEV__分支的代码
// 若需要清理 container 的子节点, 清理, 然后 new ReactRoot 并返回
function legacyCreateRootFromDOMContainer(
container: DOMContainer, // dom 节点
forceHydrate: boolean, // false render
): Root {
// 是否不需要清理 container 的子元素, 第一次 render 是 false, 即需要清理
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container); // 第一次调用时为 false
// First clear any existing content.
if (!shouldHydrate) {// 第一次 render 走这里
let warned = false;
let rootSibling;
// 这里将 container 的子元素都清理掉了
while ((rootSibling = container.lastChild)) {
container.removeChild(rootSibling);
}
}
// Legacy roots are not async by default.
const isConcurrent = false;
return new ReactRoot(container, isConcurrent, shouldHydrate);
}
这个方法比较简单,在第一次调用 ReactDOM.render 时,shouldHydrate 会是 false,所以会走到 if (!shouldHydrate) 分支里,将 container 节点的所有子节点都清理掉,最后是 new 了一个 ReactRoot 作为返回值,关于 ReactRoot 等内容将放到后面的文章分析。
小结
以上是 ReactDOM.render 的函数调用示意图。
首先在 render 方法中校验 container 参数是否合法,然后调用 legacyRenderSubtreeIntoContainer
在 legacyRenderSubtreeIntoContainer 中,调用 legacyCreateRootFromDOMContainer 拿到了 ReactRoot 的一个实例,调用 getPublicRootInstance 拿到了 instance,用于注入到 callback,和作为返回值,调用 unbatchedUpdates 开始调度过程
在 legacyCreateRootFromDOMContainer 中,首先清理了 container 中的所有子节点,然后 new 了一个 ReactRoot 并返回
TODO
unbatchedUpdates 是如何调度的
ReactRoot 是一个什么样的类
dom 的操作是在哪里