前言:最近离职准备面试,把之前写的笔记整理一下发出来,本人能力有限,如有错误的地方尽情指正
博客链接:pionpill
对应源码: github.com/facebook/re…
这节讲一下 React 如何明目张胆地偷走你的内存。
Fiber 架构那篇我们讲过,Stack 架构无法中断更新过程,因为在用户界面上出现一个介于新老 DOM 之间的状态时不可行的。但是 Fiber 架构可以将任务放在多个帧里异步执行,这就很奇怪了。Fiber 架构异步操作必然替换数据,但是数据没有显示在真实 DOM 上。中间态藏在你的内存里了!
Fiber 在更新 DOM 时会创建两棵 DOM 树。一棵是页面上显示的,用户正在使用的;另一棵是正在处理的,在异步操作过程中持续更新的。等到新的 DOM 树处理完成,就会替换老的 DOM,所有内容就被一下更新了。这种在内存中构建并直接替换的技术叫做双缓存技术。
双缓存 Fiber 树
我们将在屏幕上显示的 Fiber 树称为 current Fiber 树;在内存中构建的 Fiber 树称为 workInProgress Fiber 树。对应的 Fiber 节点命名相同。
两棵树上 Fiber 节点的 alternate 属性有如下关系:
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React 应用的根节点通过 current 指针在不同 Fiber 树的 rootFiber 间切换来实现 Fiber 树的切换。
workInProgress Fiber 树构建完成后就会交给 Renderer 渲染,当前根节点的指针指向 workInProgress Fiber 树,同时 workInProgress Fiber 树变成 current Fiber 树。
简单说一下在 mount,update 过程中 DOM 的构建与替换流程。
在 React 项目中,我们一般会在入口文件中写一段类似的代码:
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
........
</React.StrictMode>
);
首次执行 ReactDom 的 render 方法会创建 rootFiberNode 和 rootFiber:
rootFiberNode: 整个应用的根节点,只会创建一次,且不会被消除。rootFiber: 当前组件树的根节点,会被替换更新。
在我们的浏览器还没有挂载任何 DOM 时,会有如下 Fiber 树结构:
rootFiberNode --current--> rootFiber
此时 rootFiber 还没有任何子节点。当有新的 DOM 结构要被创建时,根据组件返回的 JSX 在内存中依次创建 Fiber 节点并连接成 Fiber 树,此时就会构建一棵 workInProgress Fiber 树。
构建 workInProgress Fiber 树时会复用 current Fiber 树中已有的 Fiber 节点(通过 Diff, 会专门开一章研究 Diff),所以占用的内存也不多。
rootFiberNode --> rootFiber
\--alternate--> rootFiber --> App --> div --> ...
上面当前 rootFiber 指向的 alternate rootFiber 构建完成后就会替换原有的 rootFiber。
在 update 阶段,则会有两棵 Fiber 树:
rootFiberNode --> rootFiber ----> App ----> div ---> ...
| | | |
altRootFiber -> altApp -> altDic -> alt...
同样在构建完成后替换。
Fiber 树构建过程
Fiber 树的构建起始于两个方法: performSyncWorkOnRoot 或 performConcurrentWorkOnRoot(一个同步一个异步)。
这两个方法都会开启 render 与 commit 流程,虽然同步和异步处理有些许不同,但整体上会调用以下几个核心方法:
- render 方法: 同步的
renderRootSync与异步的renderRootConcurrent - commit 方法: 都会调
commitRoot方法,异步逻辑的finishConcurrentRender方法里会调这个方法
同步构建过程
我们先简单看一下 performSyncWorkOnRoot(✨约1348行):
export function performSyncWorkOnRoot(root: FiberRoot, lanes: Lanes): null {
// 这里的 executionContext 是指当前执行上下文的标志位
// RenderContext 与 CommitContext 分别表示渲染和提交上下文
// 这里用了位运算,判断是否正处于 render 或 commit 状态
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
// 冲刷(之前没执行完的)被动副作用,这个方法返回一个布尔值
// 被动副作用是指不会直接触发组件重新渲染的操作,但是会改变组件状态或产生副作用
// 例如:事件处理程序,订阅函数,定时器回调
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects) {
// 进入这里表示还有被动副作用没做完,所以会执行异步任务处理逻辑并跳出去
ensureRootIsScheduled(root);
return null;
}
// renderRootSync 这个方法很关键,后面细说
// 这个函数的执行标志着进入 render 阶段
let exitStatus = renderRootSync(root, lanes);
// LegacyRoot 表示当前 tag 是否之前尝试过再次 render
// 这里的逻辑是: 如果首次出错, 则再尝试 render 一次。
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
const originallyAttemptedLanes = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
originallyAttemptedLanes,
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
originallyAttemptedLanes,
errorRetryLanes,
);
}
}
// 到这一步, Fiber 树已经渲染完成了
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// 进入 commit 阶段
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
workInProgressDeferredLane,
);
// 这里 render 和 commit 阶段都结束了,再次调这个方法处理异步任务
ensureRootIsScheduled(root);
return null;
}
同步逻辑还是比较简单的:
在 react 应用的初始加载过程中,会执行同步渲染模式来创建 FiberTree,因此我们看调用栈时会发现刚进入界面时一般都会有几个长任务。
异步构建过程
看一下 performConcurrentWorkOnRoot(✨约870行):
export function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean,
): RenderTaskFn | null {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
const originalCallbackNode = root.callbackNode;
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects && root.callbackNode !== originalCallbackNode) {
return null;
}
// 决定下面要做的事
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
if (lanes === NoLanes) {
// 没有要做的事,退出
return null;
}
// 判断是否开启时间切片
const shouldTimeSlice =
// 不包含阻塞的 Lane
!includesBlockingLane(root, lanes) &&
// 不包含过期的 Lane
!includesExpiredLane(root, lanes) &&
// 调度的回调函数未过期
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
// 进入同步 render 或异步 render 阶段
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
if (exitStatus !== RootInProgress) {
let renderWasConcurrent = shouldTimeSlice;
do {
if (exitStatus === RootDidNotComplete) {
// 当前 render 没有完成
markRootSuspended(root, lanes, NoLane);
} else {
// 判断并执行异步逻辑
const finishedWork: Fiber = (root.current.alternate: any);
if ( renderWasConcurrent && !isRenderConsistentWithExternalStores(finishedWork) ) {
exitStatus = renderRootSync(root, lanes);
renderWasConcurrent = false;
continue;
}
// 现在完成了一棵渲染树, 等待提交
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}
break;
} while (true);
}
ensureRootIsScheduled(root);
return getContinuationForRoot(root, originalCallbackNode);
}
react18 开始默认采用并发渲染模式,但是需要满足以下几个条件:
const shouldTimeSlice =
// 不包含阻塞的 Lane
!includesBlockingLane(root, lanes) &&
// 不包含过期的 Lane
!includesExpiredLane(root, lanes) &&
// 调度的回调函数未过期
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
为了满足这些条件,开发者需要使用并发相关的 hook,否则没法开启时间切片。
然后我们要看一下 finishConcurrentRender 方法,首先了解以下 root 的几个状态:
type RootExitStatus = 0 | 1 | 2 | 3 | 4 | 5 | 6;
const RootInProgress = 0; // 正在渲染中
const RootFatalErrored = 1; // 渲染异常
const RootErrored = 2; // 渲染异常
const RootSuspended = 3; // 挂起
const RootSuspendedWithDelay = 4; // 延迟挂起
const RootCompleted = 5; // 默认:Fiber树创建完成
const RootDidNotComplete = 6; // 未完成
然后看一下这个方法(✨约1084行):
function finishConcurrentRender(
root: FiberRoot,
exitStatus: RootExitStatus,
finishedWork: Fiber,
lanes: Lanes,
) {
// 只有 Fiber 树创建完成才继续走
switch (exitStatus) {
case xxx:
case RootCompleted: {
break;
}
default: {
throw new Error('Unknown root exit status.');
}
}
// 调 commitRoot
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes,
workInProgressDeferredLane,
);
}
总之 finishConcurrentRender 最后还是会调 commitRoot 方法,只不过会检查一下 root 的状态,判断是否有其他高优先级的任务要处理(代码没贴,有点复杂)。
我们理一下异步构建的流程: