React提供render API来实现将组件构成的视图渲染挂载到指定的DOM节点上,本文旨在梳理ReactDOM.render函数的执行逻辑,从而进一步理解React 16版本之后的Fiber以及相关处理过程(React版本是16.14.0)。
render API是属于react-dom包的,工作就是将组件渲染成DOM并挂载到DOM中,该API整体执行逻辑如下:
从上图可知render API的整体主要的处理逻辑,具体点的处理逻辑这里展开来梳理:
该函数判断挂载点是否合法,实际上就是判断挂载点是否是元素、document、documentFragement、特定的注释节点。
实际上在判断完挂载点的合法性之后,存在isContainerMarkedAsRoot函数调用,该函数判断DOM节点是否存在__reactContainer$开头的相关属性
该函数的逻辑实际上区分是初始化挂载阶段还是更新阶段,不同的阶段有不同的逻辑处理。这里暂时不看更新阶段具体的处理,其中对于初始化阶段(root为undefined)的处理,从前面梳理的逻辑图中可知是主要调用下面2个函数来做进一步的处理:
该函数实际上就是创建并返回root对象,而其直接的逻辑处理很简单:
处理挂载点下存在data-reactroot标签的元素的一些处理
调用createLegacyRoot:该函数的作用就是实例化ReactDOMBlockingRoot,即new ReactDOMBlockingRoot操作
function createLegacyRoot(container, options) {
return new ReactDOMBlockingRoot(container, 0, options);
}
function ReactDOMBlockingRoot(container, tag, options) {
this._internalRoot = createRootImpl(container, tag, options);
}
ReactDOMBlockingRoot函数实际上是作为构造函数调用的,有render、unmount这两个实例方法,其内部定义_internalRoot属性,其属性值是调用createRootImpl生成的。
createRootImpl的逻辑主要如下(剔除了与服务器渲染相关的逻辑):
function createRootImpl() {
...
var root = createContainer(container, tag, hydrate);
markContainerAsRoot(root.current, container);
var rootContainerElement = container.nodeType === COMMENT_NODE ? container.parentNode : container;
listenToAllSupportedEvents(rootContainerElement);
...
return root;
}
主要逻辑点如下:
createContainer创建root对象
通过createContainer创建root对象具体是什么?实际上背后是调用createFiberRoot函数来实现的,其源码逻辑如下:
function createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks) {
var root = new FiberRootNode(containerInfo, tag, hydrate);
// stateNode is any.
var uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
initializeUpdateQueue(uninitializedFiber);
return root;
}
FiberRootNode是一个构造函数,它的实例就是root对象即root对象是一个Node作为fiber root,这涉及到Fiber架构中的概念。
Fiber 是 React 16 中新的协调引擎,它的主要目的是使 Virtual DOM 可以进行增量式渲染。
createHostRootFiber函数的逻辑如下:
// React支持不同的模式(这是Fiber架构带来),例如Concurrent Mode、Blocking Mode等,具体模式的区别这里不细究知道就行
createFiber(3, null, null, mode);
var createFiber = function (tag, pendingProps, key, mode) {
return new FiberNode(tag, pendingProps, key, mode);
};
实例化FiberNode, Fiber本质上就是一个JavaScript对象,它存储着相关的信息。在FiberNode中有三个属性:return(指向父节点)、child(子节点)、slibling(兄弟节点),这三个属性值值可以是null或FiberNode对象,通过这三个属性就会形成类似树形结构。
之后会执行initializeUpdateQueue的逻辑,实际上就是在fiberNode对象上定义updateQueue属性。
从上面的整个处理过程可知:
在初始化挂载阶段会生成root对象(实际上就是实例化FiberRoot Node),并且会创建一个Fiber Node对象与root对象进行关联, 即root.current = FiberNode
markContainerAsRoot
该函数的就是在挂载点DOM上定义__reactContainer$开头的属性,而属性值就是当前FiberRoot的current值,即FiberNode。
listenToAllSupportedEvents
React中是合成事件机制,所有的事件都是事件委托的形式来处理的,还函数就是注册所有被支持的事件,其处理逻辑如下:
var listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
function listenToAllSupportedEvents(rootContainerElement) {
if (!rootContainerElement[listeningMarker]) {
// 添加标记避免多次绑定事件
rootContainerElement[listeningMarker] = true;
// 所有支持的事件都通过listenToNativeEvent来实现注册,其中对于selectionchange需要特殊处理
allNativeEvents.forEach(function (domEventName) {
if (domEventName !== 'selectionchange') {
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement);
}
listenToNativeEvent(domEventName, true, rootContainerElement);
}
});
var ownerDocument =
rootContainerElement.nodeType === DOCUMENT_NODE
? rootContainerElement
: rootContainerElement.ownerDocument;
// selectionchange需要注册到Document上
if (ownerDocument !== null) {
if (!ownerDocument[listeningMarker]) {
ownerDocument[listeningMarker] = true;
listenToNativeEvent('selectionchange', false, ownerDocument);
}
}
}
}
事件注册的具体逻辑这里暂不细究,只需要知道是注册所有被支持的事件到挂载点DOM(其中selectionchange注册到Document上)即可。
该函数会涉及到React中一个批量更新的机制,而在初始化挂载阶段该函数调用如下:
function unbatchedUpdates(fn, a) {
...
try {
return fn(a);
} finally {
...
}
}
unbatchedUpdates(function () {
updateContainer(element, fiberRoot, parentComponent, callback);
});
通过源码unbatchedUpdates内部实际上就是执行传入的函数,此处即调用updateContainer:
function updateContainer(element, container, parentComponent, callback) {
...
var current$1 = container.current;
...
var update = createUpdate(eventTime, lane);
update.payload = {
element: element
};
enqueueUpdate(current$1, update);
scheduleWork(current$1, expirationTime);
}
上面是简化后的逻辑,实际上updateContainer中创建一个update对象,该update对象中包含当前处理时的高精度时间等,其中payload数据中保存这着根组件对应的React元素对象,由于后续的渲染等处理。
该函数是根据相关条件排列update,实际上的处理逻辑归纳如下:
function enqueueUpdate(fiber, update) {
var updateQueue = fiber.updateQueue;
if (updateQueue === null) {
// Only occurs if the fiber has been unmounted.
return;
}
var sharedQueue = updateQueue.shared;
var pending = sharedQueue.pending;
// 处理pending与当前update之间的顺序问题
...
// updateQueue.shared.pending
sharedQueue.pending = update;
}
如果当前的fiber对象存在updateQueue,就会设置其对应的pending属性值为传入的update对象。
该函数实际上是React Fiber处理的核心逻辑,非常复杂,这里也简单聊聊针对初始化挂载阶段的主要处理逻辑有如下几点:
workLoopSync的执行逻辑如下:
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
实际上是遍历处理workInProgress Fiber对象,对workInProgress Fiber对象调用performUnitOfWork函数来处理,而performUnitOfWork函数中有2点逻辑非常重要:
function performUnitOfWork(unitOfWork) {
var next;
...
next = beginWork$1(current, unitOfWork, subtreeRenderLanes);
...
if (next === null) {
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
...
}
执行beginWork$1函数
背后调用beginWork函数,该函数的主要逻辑就是根据Fiber对象的tag调用对应的更新函数(核心参数是current Fiber 与 workInProgress Fiber)。涉及到workInProgress类型有:
switch (workInProgress.tag) {
// 不确定的组件,可能是函数组件、class组件
case IndeterminateComponent:
// lazy组件
case LazyComponent:
// 函数组件
case FunctionComponent:
// class组件
case ClassComponent:
// host tree root
case HostRoot:
case SuspenseComponent:
case Fragment:
case Profiler:
case ContextProvider:
case ContextConsumer:
case MemoComponent:
...
}
从上面实际上对应着相关的组件类型,实际上每一个组件都会对应一个FiberNode对象。这里就涉及到相关组件的实例化,例如:
class组件在源码中会调用updateClassComponent函数,而该函数中就会涉及到:constructClassInstance、mountClassInstance、finishClassComponent等逻辑,即new实例化、组件实例对象instance相关处理、完成class组件的处理
constructClassInstance函数中会触发class组件的构造函数的执行,finishClassComponent函数中会调用class组件的render函数。
workInProgress重新赋值
当beginWork$1执行完后就会返回一个值赋值给next,当next存在时就对workInProgress重新赋值,这样就会继续遍历,直到next不存在,通过设置next这个过程就可以处理到每一个组件。
commitRoot
当处理完所有组件的渲染之后,就会执行commitRoot的逻辑,而该函数的逻辑主要就是执行下面的函数:
runWithPriority$1(ImmediatePriority, commitRootImpl.bind(null, root, renderPriorityLevel));
第一个参数是优先级,第二个参数就是需要执行的回调函数,这里是commitRootImpl函数,在commitRootImpl函数中主要的逻辑是执行三个函数:
其中commitMutationEffects函数中就会将React元素对应的DOM插入到页面中,当整个阶段结束后页面上就已经显示出了组件的UI内容,但是在源码中相关逻辑还未结束,之后会执行flushSyncCallbackQueueImpl等相关函数的逻辑,至此完成整个挂载处理流程。
实际上上面的整个流程的梳理都是基于挂载阶段的处理,那么更新阶段相关处理会有什么不同吗?实际上整个流程最明显的不同点处理是legacyRenderSubtreeIntoContainer函数中,实际上在前面也有提及该函数中区分挂载阶段和更新阶段的逻辑,只是没有细究,主要的相关处理逻辑如下:
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
if (!root) {
// Initial mount
...
// Initial mount should not be batched.
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
...
updateContainer(children, fiberRoot, parentComponent, callback);
}
...
}
挂载阶段不应该批量处理,所以调用了unbatchedUpdates函数,在前面的逻辑梳理中,实际上已经知道该函数内部还是执行传递的函数参数,即主要执行updateContainer,实际上还有其他逻辑的执行,例如:
function unbatchedUpdates(fn, a) {
// 上下文设置
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
// 同步处理callback
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
flushSyncCallbackQueue();
}
}
}
而更新阶段就是直接执行updateContainer函数,而updateContainer函数可以说是非常核心的功能逻辑,主要逻辑有两点:
需要注意的是legacyRenderSubtreeIntoContainer函数的触发,并不是所有更新都是通过该函数的,该函数的调用情况还值得推敲。
对render函数的处理过程简单的梳理了下,其中涉及到Fiber的处理过程非常复杂,但是有几点明确的信息:
render这个过程中实际上最核心的就是Fiber处理,不得不说这个过程是真的非常复杂,其中涉及到的细节处理太多,理解起来还是非常困难的,特别是涉及到优先级的调度。
Render函数的整体处理流程如下: