react-dom入口函数以及基本数据类型。阅读React包的源码版本为16.8.6。
在第一章节我们了解到,react包本质上是一个数据结构建立的抽象屏障,提供起来供react的其它包,诸如react-dom,react-native调用。在这一章中,进入react-dom的源码阅读。
根据package.json的main字段入口,我们可以找到react-dom的入口文件为src/client/ReactDOM.js。我们发现该文件最后的代码export default ReactDOM仅仅对外暴露了一个对象模块。我们简单看一下这个对象模块。
// 函数内部代码均先省略
const ReactDOM: Object = {
createPortal,
findDOMNode() { },
hydrate() {},
render() {},
unstable_renderSubtreeIntoContainer() {},
unmountComponentAtNode() {},
unstable_createPortal() {},
unstable_interactiveUpdates() {},
unstable_discreteUpdates,
unstable_flushDiscreteUpdates,
flushSync,
unstable_createRoot,
unstable_createSyncRoot,
unstable_flushControlled,
}
其实这里的对象模块就是对面暴露的react-dom提供的Api部分。我们可以看到包括最熟悉的render方法,用于服务端渲染的hydrate,还有findDOMNode,createPortal等。
我们本章节就来查看下最常使用的render函数的源码大体逻辑结构。
// 调用方式 ReactDOM.render(element, container[, callback])
render(
element: React$Element<any>,
container: DOMContainer,
callback: ?Function,
) {
// 判断dom节点是否正确
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
react-dom源码中使用了flow来定义数据类型,函数入参中如element: React$Element<any>这种写法就是flow的语法。近似于typescript。
render函数在除去DEV调试部分逻辑后,剩余的代码非常简单,判断传入的container节点是否为Dom节点,是就进入legacyRenderSubtreeIntoContainer函数,我们来跟着代码接着看。
function legacyRenderSubtreeIntoContainer(
parentComponent: ?React$Component<any, any>,
children: ReactNodeList,
container: DOMContainer,
// 是否复用dom节点,服务端渲染调用
forceHydrate: boolean,
callback: ?Function,
) {
// 从 container 中获得root节点
let root: _ReactSyncRoot = (container._reactRootContainer: any);
let fiberRoot;
if (!root) {
// 没有root,创建root节点, 移除所有子节点
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
container,
forceHydrate,
);
fiberRoot = root._internalRoot;
// 有无callback
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Initial mount should not be batched.
unbatchedUpdates(() => {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
fiberRoot = root._internalRoot;
// 有无callback 逻辑同上
if (typeof callback === 'function') {
const originalCallback = callback;
callback = function() {
const instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
}
// Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
legacyRenderSubtreeIntoContainer首先取出container中的root节点,根据有无root节点来划分不同的创建更新逻辑。首次使用render函数的时候是不存在root节点的,此时通过legacyCreateRootFromDOMContainer创建一个root节点给container._reactRootContainer。然后如果存在callback就进行调用,最后进行了一个unbatchedUpdates。存在root节点的时候,就省去了创建root节点部分的代码,直接进行callback的判断和updateContainer。
我们先来看创建root节点的legacyCreateRootFromDOMContainer部分的代码。
function legacyCreateRootFromDOMContainer(
container: DOMContainer,
forceHydrate: boolean,
): _ReactSyncRoot {
const shouldHydrate =
forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
// First clear any existing content.
// 不需要进行 shouldHydrate 过程,即我们正常的render过程
if (!shouldHydrate) {
let warned = false;
let rootSibling;
// 当有子节点的时候,一直循环,删除完子节点
while ((rootSibling = container.lastChild)) {
container.removeChild(rootSibling);
}
}
// Legacy roots are not batched.
/**
* LegacyRoot 为一个常量标识符,具体细节如下
* export type RootTag = 0 | 1 | 2;
* export const LegacyRoot = 0;
* export const BatchedRoot = 1;
* export const ConcurrentRoot = 2;
*/
return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
}
前面提到过,forceHydrate这个布尔值是用于标识是否是服务端渲染的,在浏览器环境下是不触碰这部分的逻辑的,这个相关部分就先跳过。那么legacyCreateRootFromDOMContainer就做了两件事情:
- 删除
container容器部分的所有子节点。这也就是为什么我们使用ReactDom.render渲染在目标节点之后,节点的子元素全部消失的原因。 - 返回了
ReactSyncRoot类,实例化了一个root根节点的实例。
接下来的ReactSyncRoot代码更简单:
function ReactSyncRoot(
container: DOMContainer,
tag: RootTag,
hydrate: boolean,
) {
// Tag is either LegacyRoot or Concurrent Root
const root = createContainer(container, tag, hydrate);
this._internalRoot = root;
}
我们追寻createContainer函数,发现这个函数文件在react-reconciler/src/ReactFiberReconciler包中。我们跟着去查看一下:
export function createContainer(
containerInfo: Container,
tag: RootTag,
hydrate: boolean,
): OpaqueRoot {
return createFiberRoot(containerInfo, tag, hydrate);
}
// 在 `react-reconciler/src/ReactFiberRoot`文件中
export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(tag);
// 相互指
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
return root;
}
// fiber root 结构的真身
function FiberRootNode(containerInfo, tag, hydrate) {
this.tag = tag;
// root 节点对应的Fiber对象
this.current = null;
// dom 节点
this.containerInfo = containerInfo;
// 持久化更新会用到
this.pendingChildren = null;
this.pingCache = null;
this.finishedExpirationTime = NoWork;
this.finishedWork = null;
this.timeoutHandle = noTimeout;
this.context = null;
this.pendingContext = null;
this.hydrate = hydrate;
this.firstBatch = null;
this.callbackNode = null;
this.callbackExpirationTime = NoWork;
this.firstPendingTime = NoWork;
this.lastPendingTime = NoWork;
this.pingTime = NoWork;
if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
this.memoizedInteractions = new Set();
this.pendingInteractionMap = new Map();
}
}
终于在FiberRootNode中发现了rootRoot的真身,就是一个带标识的对象。其中比较重要的一个为containerInfo,就是reactElement将要渲染上的容器节点信息。我们还能发现,很多标识赋值了NoWork,NoWork设计到后续我们更新会提及的ExpirationTime的概念,是React更新算法的基础。目前你可以就把NoWork理解为一个标识0的常量(源码export const NoWork = 0;)。
我们最后来看current,在createFiberRoot中将其指向了createHostRootFiber创建的uninitializedFiber。这个uninitializedFiber就是reactElement对应的fiber节点,我们一起来看一下这部分逻辑。
// 位于react-reconciler/src/ReactFiber.js
function createHostRootFiber(tag: RootTag): Fiber {
let mode;
// 根据 tag 的不同,获得不同的mode模式
if (tag === ConcurrentRoot) {
mode = ConcurrentMode | BatchedMode | StrictMode;
} else if (tag === BatchedRoot) {
mode = BatchedMode | StrictMode;
} 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;
}
return createFiber(HostRoot, null, null, mode);
}
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);
};
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
this.ref = null;
// pendingProps 将要更新
this.pendingProps = pendingProps;
// 之前的props
this.memoizedProps = null;
// update对象
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects,标记组件生命周期,以及组件是否需要更新
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
this.expirationTime = NoWork;
this.childExpirationTime = NoWork;
this.alternate = null;
if (enableProfilerTimer) {
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
// It's okay to replace the initial doubles with smis after initialization.
// This won't trigger the performance cliff mentioned above,
// and it simplifies other profiler code (including DevTools).
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
}
这部分逻辑比较长,我们来拆成两部来看。createHostRootFiber总共做了两件事情,根据tag存在的标识,调整了mode字段。然后使用mode字段创建了FiberNode对象。
这里我们稍微提一下使用|和&来进行一个打标的设计模式。比如我现在有三个属性的标识符a/b/c,我们用二进制来定义它们,保证每个模式1所在的位置不同。
var a = 0b001;
var b = 0b010;
var c = 0b100;
我们现在对一个demo变量进行属性的赋值,比如我想要这个demo变量拥有属性a和属性c。那我只需要var demo = a | c。在后续我对demo进行一个拥有属性判断的时候,我只需要使用&,如果得到的结果大于0,即转换为true,就说明demo拥有该属性。如我想要判断demo是否含有a属性,只需要if (demo | a) { /* ... */ }即可。如果我想要给demo添加一个属性,比如添加属性b,只需要将demo |= b即可,如果不是很了解一元操作符的同学,可以去mdn上面查一下相关的资料就能明白。
我们前面在legacyCreateRootFromDOMContainer函数的注释中提到过,rootTag是通过LegacyRoot | BatchedRoot | ConcurrentRoot取得的三个模式的综合。所以createHostRootFiber这里我们走的是最后一个else分支,mode=NoMode。然后创建Fiber节点。
Fiber节点就是对应每一个ReactElement的节点了,它上面记载了很多我们熟悉的属性,比如ref,比如props相关的pendingProps,memoizedProps。然后还需要关注一下的概念就是expirationTime。expirationTime前面root的时候也提到了,这是节点更新操作的依据,在后续的源码部分也会单独拆分一章节来阐述它。
还需要提一下的是我注释了Fiber相关的几个属性sibling,return,child。react中的Fiber节点对应的是一个单向列表结构。比如我有这样的一个jsx结构:
function Demo() {
return (
<ul>
<li></li>
<li></li>
<li></li>
</ul>
);
}
那么这个结构在Fiber中会这样存在ul.child -> li(1).sibling -> li(2).sibling -> li(3)。每个节点的return则对应总的父节点li(1).return -> ul。
这一章当中,我们简单看了一下ReactDom.render的总体函数逻辑和创建数据结构部分的源码。首次创建的时候,render会创建一个FiberRootNode对象,该对象作为整个React应用的根节点,同时给RootNode创建对应的Fiber对象。每一个Fiber对象对应一个ReactElement元素,它带有各种用于React调度的属性元素,DOM以单向链表的数据结存在于React应用当中。
下一章我们会接着render函数的逻辑进入unbatchedUpdates部分代码,大体介绍一下React-dom在更新中的一些框架设计。

本文主要探讨React DOM的入口函数及数据类型,重点关注react-dom Render函数的源码。在React 16.8.6版本中,我们分析了ReactDOM.render()的逻辑结构,包括清理容器、创建DOM节点、构建root实例等关键步骤。通过源码阅读,揭示React应用中root节点、fiber节点及其属性的重要性,为后续理解React更新机制奠定基础。
7211

被折叠的 条评论
为什么被折叠?



