ReactDOM.render 串联渲染链路 —— 初始化阶段
拆解之前的Render调用栈——初始化阶段生成Fiber节点
ReactDOM.render 串联渲染链路概览
首先我们提取出初始化过程中获取Fiber节点时涉及的调用栈大图:
可以去源码里找到
legacyRenderSubtreeIntoContainer
这个方法,如果是用 create-react-app
创建的项目,可以去 node_modules -> react-dom -> cjs -> react-dom.development.js 搜索。 legacyRenderSubtreeIntoContainer 的关键逻辑如下(解析在注释里):
function legacyRenderSubtreeIntoContainer(parentComponent, children, container, forceHydrate, callback) {
// container 是传入的真实的 DOM 对象
var root = container._reactRootContainer;
// 初始化 fiberRoot
var fiberRoot;
// 因为此时 container 是我们传入的真实 DOM 对象, 不存在 _reactRootContainer 属性, 所以 root 为空
if (!root) {
// 如果 root 为空,就初始化 _reactRootContainer, 然后再赋给 root
root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);
// 通过legacyCreateRootFromDOMContainer这个方法创建的对象有一个 _internalRoot 属性。将它赋值给 fiberRoot
fiberRoot = root._internalRoot;
//console.log("root", root);
//console.log("fiberRoot", fiberRoot);
// 这里处理 ReactDOM.render 中传递的回调函数。如果为 render方法提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。
if (typeof callback === 'function') {
var originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
originalCallback.call(instance);
};
} // Initial mount should not be batched.
// 进入 unbatchedUpdates 方法
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
} else {
// 进入 else 逻辑就证明 root 不为空, 存在 _reactRootContainer,
// 即并不是初次渲染(那么就是更新了)。其中工作除了跳过初始化,基本和上一代码块一致
fiberRoot = root._internalRoot;
if (typeof callback === 'function') {
var _originalCallback = callback;
callback = function () {
var instance = getPublicRootInstance(fiberRoot);
_originalCallback.call(instance);
};
} // Update
updateContainer(children, fiberRoot, parentComponent, callback);
}
return getPublicRootInstance(fiberRoot);
}
画个流程图总结一下:
我们需要关注到 fiberRoot 这个对象我们可以将 root 和 fiberRoot 打印到控制台看一下
这个就是 root,也就是 _reactRootContainer。然后其中的 _internalRoot 属性赋给了 fiberRoot虽然 current 对象包含了很多属性,但需要注意 current 对象是一个 FiberNode 实例,不仅如此,它还是当前 Fiber 树的头部节点。
现在应该有这样的一个指向关系:
- fiberRoot 的关联对象是真实 DOM 的容器节点
- rootFiber(也就是 fiberRoot 的 current 属性) 则作为虚拟 DOM 的根节点存在
FiberNode 对象属性简单概览
function FiberNode(tag, pendingProps, key, mode) {
// 作为静态数据结构的属性
// Fiber对应的组件类型 Funtion/Class/...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况和下面的type相同,某些情况不同,比如FunctionComponent使用React.memo包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.type = null;
// Fiber对应的真实DOM节点
this.stateNode = null; // Fiber
// 用于连接其他Fiber节点形成Fiber树
this.return = null; // 指向父级Fiber节点
this.child = null; // 指向子级Fiber节点
this.sibling = null; // 指向右边第一个兄弟Fiber节点
this.index = 0;
this.ref = null;
// 作为动态的工作单元属性
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode; // Effects
this.flags = NoFlags;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
..............
}
进入updateContainer 方法
生成了 fiberRoot 后接着就是和 ReactDOM.render 方法的其他入参一起,被传入 updateContainer 方法,形成一个回调。这个回调,正是接下来要调用的 unbatchedUpdates 方法的入参。
// 进入 unbatchedUpdates 方法
unbatchedUpdates(function () {
updateContainer(children, fiberRoot, parentComponent, callback);
});
我们先来看看 unbatchedUpdates
这个函数,它拿回调去干嘛了。
下面代码是对 unbatchedUpdates 主体逻辑的提取:
function unbatchedUpdates(fn, a) {
// 这里是对上下文的处理,不必纠结
var prevExecutionContext = executionContext;
executionContext &= ~BatchedContext;
executionContext |= LegacyUnbatchedContext;
try {
// 重点在这里,直接调用了传入的回调函数 fn,对应当前链路中的 updateContainer 方法
return fn(a);
} finally {
// finally 逻辑里是对回调队列的处理,此处不用太关注
executionContext = prevExecutionContext;
if (executionContext === NoContext) {
// Flush the immediate callbacks that were scheduled during this batch
resetRenderTimer();
flushSyncCallbackQueue();
}
}
简而言之,它直接调用了传入的回调 fn。而在当前链路中,fn 是一个针对 updateContainer 的调用。
所以,接下来我们很有必要去看看 updateContainer 里面的逻辑。
function updateContainer(element, container, parentComponent, callback) {
//......
// 这里的 container 就是 fiberRoot (fiberRootNode对象), 而 fiberRoot.current 就是 fiberNode 对象
var current$1 = container.current;
// 这是一个 event 相关的入参,此处不必关注
var eventTime = requestEventTime();
// ......
// 这是一个比较关键的入参,lane 表示优先级
var lane = requestUpdateLane(current$1);
// 结合 lane(优先级)信息,创建 update 对象,一个 update 对象意味着一个更新
var update = createUpdate(eventTime, lane);
// update 的 payload 对应的是一个 React 元素
update.payload = {
element: element
};
// 处理 callback,这个 callback 其实就是我们调用 ReactDOM.render 时传入的 callback
callback = callback === undefined ? null : callback;
if (callback !== null) {
{
if (typeof callback !== 'function') {
error('render(...): Expected the last optional `callback` argument to be a ' + 'function. Instead received: %s.', callback);
}
}
update.callback = callback;
}
// 将 update 入队
enqueueUpdate(current$1, update);
// 调度 fiberRoot 更新,在这个函数中,会处理一系列与优先级、打断操作相关的逻辑。
scheduleUpdateOnFiber(current$1, lane, eventTime);
// 返回当前节点(fiberRoot)的优先级
return lane;
}
总结一下 updateContainer
的逻辑,它做的最关键的事情可以总结为三件:
- 获取当前 Fiber 节点的 lane (优先级)
- 结合 lane (优先级),创建当前 Fiber 节点的 update 对象,并将其入队列
- 调度当前节点 (rootFiber)
这个时候我们再看看,scheduleUpdateOnFiber 的调用栈
可以发现这些函数都有一个 Sync (同步) 的字样而现在,我相信你心里更多的疑惑在于:都说 Fiber 架构带来的异步渲染是 React 16 的亮点,为什么分析到现在,竟然发现 ReactDOM.render 触发的首次渲染是个同步过程呢?
其实,React 有一个全新的 Concurrent 模式,只不过这个模式尚处于实验性阶段。
这个实验性阶段的模式有什么用呢?这里简要概括一下官方的解释:
非Concurrent 模式下,一旦React开始渲染一次更新,它们不能中断包括创建新的 DOM 节点和运行组件中代码在内的工作。我们称这种方法为 “阻塞渲染”。
在 Concurrent 模式中,渲染不是阻塞的。它是可中断的。
在这就先不过多解释了(避免文章内容太多,主次冲突)。对于 Concurrent 模式的详细分析可以点击这里查看。
这里简单说一下如何启动 Concurrent 模式,并看一下和现有模式的区别
官方文档看这里:使用 Concurrent 模式(实验性)
- 安装:
npm install react@experimental react-dom@experimental
- 在整个 <App /> 结构树里启用 concurrent 模式:
import ReactDOM from 'react-dom';
// 如果你之前的代码是:
//
// ReactDOM.render(<App />, document.getElementById('root'));
//
// 你可以用下面的代码引入 concurrent 模式:
ReactDOM.unstable_createRoot(
document.getElementById('root')
).render(<App />);
这个时候我们对比一下两次的函数调用栈记录
上面这三个是开启了 Concurrent 模式的记录
这是未开启 Concurrent 模式的记录
我们可以发现, performConcurrntWorkOnRoot
相比于 performSyncWorkOnRoot
最大的区别在于,调用的环境不同。performSyncWorkOnRoot
是在 schedule 调度Fiber节点更新后调用。而performConcurrntWorkOnRoot
是在 Pares HTML (解析 HTML后) 通过回调函数调用的。而且还是多段的,分片的(我觉得这应该就是非阻塞可中断的表现)。
总结
渲染链路的初始化阶段的工作就是
- 通过传入的真实 DOM 生成 fiberRoot(FiberRootNode实例), fiberRoot 关联对象是真实 DOM 的容器节点,同时它还有个current 属性,这个current属性,我们可以把它称之为 rootFiber(FiberNode实例),它作为虚拟 DOM 的根节点存在。
- 获取当前节点(rootFiber)的 lane (优先级),结合 lane (优先级),创建当前节点(rootFiber)的 update 对象,并将其入队列。
- 调度当前节点 (rootFiber)。结束初始化阶段准备进 渲染阶段。