前言
在React开发中不可避免 我们总要用到setState 或者 useState相关以此来更新界面,但是在渲染中究竟发生了什么呢?让我们带着疑问一步一步看看。为了让读者看的明白清楚 我内容一分为三。
在看这篇文章时,可结合 React 渲染流程可视化进行服用。
- Fiber到底是什么
- 组件到底是怎么render的
- commit 阶段react做了哪些事情
挂载(commitRoot)
flags (effectTag)
从上一篇文章render我们也对 flags 有了小小的了解。需要被操作的 Fiber 都会被打上 flags 标记。
var NoFlags =
/* */
0;
var Placement =
/* */
2;
var Update =
/* */
4;
var ChildDeletion =
/* */
16;
在以前的版本中还存在
var PlacementAndUpdate =
/* */
6;
var Deletion =
/* */
8;
根据上面我们很容易发现 PlacementAndUpdate 是通过Placement 和 Update 相加得出的为0 ,在查看源码我们也发现通过按位与运算来判断是何种改变而后对dom进行操作。(新版本中)
在React 18中,提交阶段 commitRoot 的处理有所改变。
在React 17及之前的版本中,React在完成一次更新(也就是构建完current Fiber树)后,会把所有需要执行副作用的fiber节点用链表串联起来,形成effect list。这样做的目的是为了在commit阶段能快速找到所有有副作用的fiber节点,进行相应的DOM操作。这个链表的第一个节点存储在finishedWork.firstEffect,通过每个节点上的nextEffect可以遍历整个链表。
但在React 18以后,这个方案改变了,现在React在commit阶段不再使用effect list。取而代之的是,在新的Fiber架构中所有fiber节点的child、sibling和return指针形成的树形结构。此时commit阶段的处理方式变为深度优先遍历这个Fiber树。这是为了配合React 18新引入的并发模式(concurrent mode)和其他的新特性。
在React 18中,finishedWork上的firstEffect字段被废弃,原本存放在各个Fiber节点effectTag上的副作用标识,改为存放在flags字段上,不再使用effectTag。
当读完上面的文字,我们会有一些疑问。在render阶段已经进行了深度遍历。已经找到了需要改变fiber并且打上了标签。如果在commit阶段还要进行深度遍历一次。这不是资源浪费吗?
的确,在React 17及之前的版本中,React在render阶段就已经遍历过一次Fiber树,并在此过程中标记了哪些Fiber节点有副作用,然后将这些有副作用的节点用链表串联起来,在commit阶段只需要遍历这个链表即可。这确实会比在commit阶段重新遍历整个Fiber树要性能更好。
为什么在React 18中,React仍然改回在commit阶段遍历整个Fiber流程,这在初看起来似乎是一种效率损失,但这实际上是React在为了引入新的并发模式做的权衡和妥协。
在React 17之前,React并没有真正的并发机制。虽然React 16开始引入了Fiber机制,可以打断任务和恢复任务,但仍无法同时处理多个任务。而React 18带来了并发模式(concurrent mode),此模式下,React可以在内存中同时处理多个任务。
并发模式下,可能存在多个任务同时在内存中进行,每个任务都可能产生不同的更新和副作用,因此会有多个不同的Fiber树在内存中同时存在。如果还像React 17那样只通过链表来管理副作用,那么这个链表将会变得非常复杂,因为需要同时管理多个任务产生的副作用。
相比之下,如果在commit阶段遍历整个Fiber树,虽然在性能上可能较17有所损失,但在代码复杂度和可维护性上有了提升。并且,对于绝大多数应用来说,这样的性能损失是可以接受的,因为commit阶段通常只占整个更新过程的一小部分时间。
所以 React团队是在“在commit阶段节约CPU时间”和“支持并发模式,简化代码维护难度”之间做出了权衡。再加上通过对React的调度方式进行优化,从综合效率上来看,并不会造成大的性能损失。
为了便于好理解,这里我们对 React 18以前的处理方式进行讲解。去除别的干扰。
effectList
为了便于好理解 下面我举一个例子。
function App() {
const [count, setCount] = useState(0);
return (
<div onClick={() => setCount(count + 1)}>
<p>{count}</p>
<span>{count}</span>
</div>
);
};
当我们 执行例子,点击div 时,在 performSyncWorkOnRoot 方法 的末尾的 finishedWork的 firstEffect 我们会发现 当前的effectList 是类似于下面的情况。
在例子中,我们通过 setCount 进行重新渲染界面。 p 和span 因为count发生了变化重新渲染。div 因重新创建了 onClick 也重新渲染。因为是深度遍历所以 firstEffect为 p。
function commitRoot(root) {
const renderPriorityLevel = getCurrentPriorityLevel();
runWithPriority(
ImmediateSchedulerPriority,
commitRootImpl.bind(null, root, renderPriorityLevel),
);
return null;
}
在commitRoot中同时使用到了渲染优先级和调度优先级,本节不再赘述优先级. 最后的实现是通过commitRootImpl函数。
// ... 省略部分无关代码
function commitRootImpl(root, renderPriorityLevel) {
// ============ 渲染前: 准备 ============
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
// 清空FiberRoot对象上的属性
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
if (root === workInProgressRoot) {
// 重置全局变量
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
// 再次更新副作用队列
let firstEffect;
if (finishedWork.flags > PerformedWork) {
// 默认情况下fiber节点的副作用队列是不包括自身的
// 如果根节点有副作用, 则将根节点添加到副作用队列的末尾
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
firstEffect = finishedWork.firstEffect;
}
// ============ 渲染 ============
let firstEffect = finishedWork.firstEffect;
if (firstEffect !== null) {
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// 阶段1: dom突变之前
nextEffect = firstEffect;
do {
commitBeforeMutationEffects();
} while (nextEffect !== null);
// 阶段2: dom突变, 界面发生改变
nextEffect = firstEffect;
do {
commitMutationEffects(root, renderPriorityLevel);
} while (nextEffect !== null);
// 恢复界面状态
resetAfterCommit(root.containerInfo);
// 切换current指针
root.current = finishedWork;
// 阶段3: layout阶段, 调用生命周期componentDidUpdate和回调函数等
nextEffect = firstEffect;
do {
commitLayoutEffects(root, lanes);
} while (nextEffect !== null);
nextEffect = null;
executionContext = prevExecutionContext;
}
// ============ 渲染后: 重置与清理 ============
if (rootDoesHavePassiveEffects) {
// 有被动作用(使用useEffect), 保存一些全局变量
} else {
// 分解副作用队列链表, 辅助垃圾回收
// 如果有被动作用(使用useEffect), 会把分解操作放在flushPassiveEffects函数中
nextEffect = firstEffect;
while (nextEffect !== null) {
const nextNextEffect = nextEffect.nextEffect;
nextEffect.nextEffect = null;
if (nextEffect.flags & Deletion) {
detachFiberAfterEffects(nextEffect);
}
nextEffect = nextNextEffect;
}
}
// 重置一些全局变量(省略这部分代码)...
// 下面代码用于检测是否有新的更新任务
// 比如在componentDidMount函数中, 再次调用setState()
// 1. 检测常规(异步)任务, 如果有则会发起异步调度(调度中心`scheduler`只能异步调用)
ensureRootIsScheduled(root, now());
// 2. 检测同步任务, 如果有则主动调用flushSyncCallbackQueue(无需再次等待scheduler调度), 再次进入fiber树构造循环
flushSyncCallbackQueue();
return null;
}
上面的代码比较多 我们可以先不用完全理解,在这个代码中我们把这个过程分为三个阶段:
- 渲染前
- 渲染
- 渲染后
渲染前
function commitRootImpl(root, renderPriorityLevel) {
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// ...
}
在方法起始阶段,我们看到方法先判断 rootWithPendingPassiveEffects 是否为 null,如果不为 null 就会执行 flushPassiveEffects。
这个 PassiveEffects 指的就是 那些被推迟执行的被动效应(passive effects),也就是由 useEffect hook 创建的副作用函数。这里我们要清楚一个概念:useEffect 中的副作用函数是在渲染到真实 DOM 后才会执行的。
这里的判断执行的是当前是否还有未执行的 useEffect,如果有,就执行它,也就是说在开启新一轮的 commit 阶段时会先等待上一轮的 useEffect 执行完。
function commitRootImpl(root, renderPriorityLevel) {
// ....
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
// 清空FiberRoot对象上的属性
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
if (root === workInProgressRoot) {
// 重置全局变量
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
}
// ...
}
接着会重置 render阶段使用到的一些全局变量。表示准备开始渲染工作。
function commitRootImpl(root, renderPriorityLevel) {
// ...
// 再次更新副作用队列
let firstEffect;
if (finishedWork.flags > PerformedWork) {
// 默认情况下fiber节点的副作用队列是不包括自身的
// 如果根节点有副作用, 则将根节点添加到副作用队列的末尾
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
firstEffect = finishedWork.firstEffect;
}
// ...
}
上面说过 render阶段 已经把带有 effectTag 的 fiber 节点连接形成一条链表了,这里再次处理 effect list 是因为这条链表目前只有子节点,并没有挂载根节点。如果根节点也存在 effectTag,那么就需要把根节点拼接到链表的末尾,形成一条完整的 effect list。
总结来说渲染前做了以下操作:
-
处理上次更新的副作用队列
-
设置全局状态(如: 更新fiberRoot上的属性) 重置全局变量(如: workInProgressRoot, workInProgress等)
-
再次更新副作用队列: 只针对根节点fiberRoot.finishedWork
- 默认情况下根节点的副作用队列是不包括自身的, 如果根节点有副作用, 则将根节点添加到副作用队列的末尾
- 注意只是延长了副作用队列, 但是fiberRoot.lastEffect指针并没有改变.
渲染中
commitRootImpl函数中, 渲染阶段的主要逻辑是处理副作用队列, 将最新的 虚拟DOM 节点渲染到真实dom上。
整个渲染过程被分为 3 个阶段:
-
commitBeforeMutationEffects
dom 变更之前, 处理副作用队列中带有
Snapshot,Passive标记的fiber节点。 -
commitMutationEffects
dom 变更, 界面得到更新. 处理副作用队列中带有
Placement,Update,Deletion,Hydrating标记的fiber节点。 -
commitLayoutEffects
dom 变更后, 处理副作用队列中带有
Update|Callback标记的fiber节点。
commitBeforeMutationEffects
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const current = nextEffect.alternate;
const flags = nextEffect.flags;
// 处理`Snapshot`标记
if ((flags & Snapshot) !== NoFlags) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
// 处理`Passive`标记
if ((flags & Passive) !== NoFlags) {
// Passive标记只在使用了hook, useEffect会出现. 所以此处是针对hook对象的处理
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
我们再看看 commitBeforeMutationEffectOnFiber 做了什么事情
function commitBeforeMutationLifeCycles(
current: Fiber | null,
finishedWork: Fiber,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
return;
}
case ClassComponent: {
if (finishedWork.flags & Snapshot) {
if (current !== null) {
const prevProps = current.memoizedProps;
const prevState = current.memoizedState;
const instance = finishedWork.stateNode;
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
}
return;
}
case HostRoot: {
if (supportsMutation) {
if (finishedWork.flags & Snapshot) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
}
return;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
return;
}
}
从源码中可以看出, 与Snapshot标记相关的类型只有ClassComponent和HostRoot.
- 对于
ClassComponent类型节点, 调用了instance.getSnapshotBeforeUpdate生命周期函数。 - 对于
HostRoot类型节点, 调用clearContainer清空了容器节点(即div#root这个 dom 节点)。
在处理完 Snapshot 以后, 接下来就是处理 Passive 标记了。但是要注意这里并不是立即执行,而是把它放在 scheduleCallback 的回调当中,scheduleCallback方法会以一个优先级异步执行它的回调函数。
我们说清楚一些就是,如果存在 Passive ,则把 rootDoesHavePassiveEffects 置为 true,并且调度 flushPassiveEffects,而整个 commit阶段 是同步执行 的,所以 useEffect 的回调函数其实会在 commit阶段 完成后 再异步执行。
我们按照上面说的做一个小测试 (答案在最底部)
function App() {
console.log(1);
useEffect(() => {
console.log(2);
});
console.log(3);
Promise.resolve(() => {
console.log(4);
});
return <div>test</div>;
}
commitMutationEffects
这个阶段 dom 变更, 界面得到更新, 处理副作用队列中带有ContentReset, Ref, Placement, Update, Deletion, Hydrating 标记的fiber节点.
// ...省略部分无关代码
function commitMutationEffects(
root: FiberRoot,
renderPriorityLevel: ReactPriorityLevel,
) {
// 处理Ref
if (flags & Ref) {
const current = nextEffect.alternate;
if (current !== null) {
// 先清空ref, 在commitRoot的第三阶段(dom变更后), 再重新赋值
commitDetachRef(current);
}
}
// 处理DOM突变
while (nextEffect !== null) {
const flags = nextEffect.flags;
const primaryFlags = flags & (Placement | Update | Deletion | Hydrating);
switch (primaryFlags) {
case Placement: {
// 新增节点
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement; // 注意Placement标记会被清除
break;
}
case PlacementAndUpdate: {
// Placement
commitPlacement(nextEffect);
nextEffect.flags &= ~Placement;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
// 更新节点
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Deletion: {
// 删除节点
commitDeletion(root, nextEffect, renderPriorityLevel);
break;
}
}
nextEffect = nextEffect.nextEffect;
}
}
处理 DOM 有以下集中情况:
-
新增: commitPlacement -> insertOrAppendPlacementNode -> appendChild
-
更新: commitWork -> commitUpdate
-
删除: commitDeletion -> removeChild
最终会调用appendChild, commitUpdate, removeChild, 它们是HostConfig协议(源码在 ReactDOMHostConfig.js 中)中规定的标准函数, 在渲染器react-dom包中进行实现. 这些函数就是直接操作 DOM, 所以执行之后, 界面也会得到更新。
注意: commitMutationEffects执行之后, 在commitRootImpl函数中切换当前fiber树(root.current = finishedWork),保证fiberRoot.current指向代表当前界面的fiber树.
commitLayoutEffects
dom 变更后, 处理副作用队列中带有Update, Callback, Ref标记的fiber节点。
// ...省略部分无关代码
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const flags = nextEffect.flags;
// 处理 Update和Callback标记
if (flags & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
if (flags & Ref) {
// 重新设置ref
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}
核心逻辑都在commitLayoutEffectOnFiber->commitLifeCycles函数中.
// ...省略部分无关代码
function commitLifeCycles(
finishedRoot,
current,
finishedWork,
committedLanes,
): void {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block:
{
{
commitHookEffectListMount(Layout | HasEffect, finishedWork);
}
schedulePassiveEffects(finishedWork);
return;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (current === null) {
// 初次渲染: 调用 componentDidMount
instance.componentDidMount();
} else {
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(finishedWork.type, current.memoizedProps);
const prevState = current.memoizedState;
// 更新阶段: 调用 componentDidUpdate
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
const updateQueue: UpdateQueue<*> | null =
(finishedWork.updateQueue: any);
if (updateQueue !== null) {
// 处理update回调函数 如: this.setState({}, callback)
commitUpdateQueue(finishedWork, updateQueue, instance);
}
return;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
// 设置focus等原生状态
commitMount(instance, type, props, finishedWork);
}
return;
}
}
}
-
FunctionComponent
-
会把
HookLayout这个 tag 类型传给commitHookEffectListMount方法,也就是说这里会执行useLayoutEffect的回调函数。 -
接着会执行
schedulePassiveEffects方法,在这里会分别注册useEffect销毁函数和回调函数,其实也就是把effect分别推进pendingPassiveHookEffectsUnmount和pendingPassiveHookEffectsMount这两个数组中,后续 取出来执行。
-
-
ClassComponent
- 如果
current为空,也就是这个节点是首次 render,则会执行它的componentDidMount生命周期方法,否则会执行componentDidUpdate方法。 - 处理
update回调函数 如:this.setState({}, callback)。
- 如果
-
HostComponent
- 如果有
Update标记, 需要设置一些原生状态(如:focus等)
- 如果有
渲染后
执行完上面内容以后, 渲染任务就已经完成了。 在渲染完成后, 需要做一些重置和清理工作:
-
清除副作用队列
- 由于副作用队列是一个链表, 由于单个fiber对象的引用关系, 无法被gc回收.
- 将链表全部拆开, 当fiber对象不再使用的时候, 可以被gc回收.
-
检测更新
- 在整个渲染过程中, 有可能产生新的update(比如在
componentDidMount函数中, 再次调用setState())。 - 如果是常规(异步)任务, 不用特殊处理, 调用
ensureRootIsScheduled确保任务已经注册到调度中心即可。 - 如果是同步任务, 则主动调用
flushSyncCallbackQueue(无需再次等待 scheduler 调度), 再次进入fiber树构造循环。
- 在整个渲染过程中, 有可能产生新的update(比如在
答案
输出顺序为 1, 3, 4, 2