Appearance
架构篇
第三章 render 阶段
流程概览
创建与构建 Fiber 树
在 React 中,Fiber 架构是一个关键组成部分,它涉及两个主要步 骤:performSyncWorkOnRoot和performConcurrentWorkOnRoot。 根据是同步更新还是异步更新,它们负 责调用特定的工作循环方法。
工作循环
两个核心函数是**workLoopSync和workLoopConcurrent,它们区别在于shouldYield**的使用。
workLoopSync不检查是否需要让出浏览器主线程。
js
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}workLoopConcurrent在浏览器帧没有剩余时间时,会中断循环。
js
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}创建 Fiber 节点:PerformUnitOfWork
**performUnitOfWork**负责创建新的 Fiber 节点并链接至现有 Fiber 树。
js
function performUnitOfWork(fiber) {
// 执行beginWork
beginWork(fiber);
if (fiber.child) {
performUnitOfWork(fiber.child);
}
// 执行completeWork
completeWork(fiber);
if (fiber.sibling) {
performUnitOfWork(fiber.sibling);
}
}function performUnitOfWork(fiber) {
// 执行beginWork
beginWork(fiber);
if (fiber.child) {
performUnitOfWork(fiber.child);
}
// 执行completeWork
completeWork(fiber);
if (fiber.sibling) {
performUnitOfWork(fiber.sibling);
}
}“递”与“归”阶段
- 递阶段(Descending Phase): 从**
rootFiber开始,对每个节点调用beginWork**。子 Fiber 节点会 在这里被创建并链接。 - 归阶段(Ascending Phase): 当遍历到叶子节点时,会调用**
completeWork**来完成工作。
示例
考虑以下 React 应用代码:
js
function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
);
}function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
);
}在**render**阶段,以下步骤会依次执行:
- rootFiber beginWork
- App Fiber beginWork
- div Fiber beginWork
- "i am" Fiber beginWork
- "i am" Fiber completeWork
- span Fiber beginWork
- span Fiber completeWork
- div Fiber completeWork
- App Fiber completeWork
- rootFiber completeWork
注意点
“KaSong” Fiber 没有进行 beginWork/completeWork,这是因为React 针对只有单一文本子节点的 Fiber 进行 了优化。
总结
了解这些概念和工作流程对于深入理解 React 的内部机制非常有帮助。这里只是一个概览,实际应用还需要更多 的细致探究。
bginWork
在 React 的源码中,beginWork 函数在 render 阶段扮演着关键角色。这个函数负责处理 Fiber 节点,它 接受三个参数:current,workInProgress,和**renderLanes**。
beginWork 函数参数
current:当前组件对应的上一次更新时的 Fiber 节点,也即**workInProgress.alternate**。workInProgress:当前组件对应的 Fiber 节点。renderLanes:与优先级相关,这一部分在讲解 Scheduler 时会深入探讨。
区分 Mount 和 Update
通过检查**current === null**,可以判断组件是处于 mount 状态还是 update 状态。
Update 时
当**current !== null时,这意味着可能存在优化路径。在特定条件下,可以复用current**节点。这些条件 包括:
- props 和 fiber.type 没有改变。
- 当前 Fiber 节点的优先级不够。
满足这些条件时,**didReceiveUpdate**将设置为 false。
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps || workInProgress.type !== current.type) {
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps !== newProps || workInProgress.type !== current.type) {
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}Mount 时
在 mount 阶段,current === null。在这种情况下,会根据**workInProgress.tag**来创建不同类型的 子 Fiber 节点。
核心操作:reconcileChildren
这个函数是 Reconciler 模块的核心。它负责创建新的子 Fiber 节点(mount)或对比现有的子 Fiber 节点并生 成新的子 Fiber 节点(update)。
js
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}effectTag
**effectTag存储了要执行的 DOM 操作的类型。**在 commit 阶段,会根据这个 tag 执行具体的 DOM 操作。
特别注意:首屏渲染
首屏渲染是一个特例,因为在这个阶段,整棵 Fiber 树的所有节点理论上都会有 Placement effectTag, 这会导致大量不必要的 DOM 插入操作。为了解决这个问题,在首屏渲染时,只有 rootFiber 会被赋予 Placement effectTag。
结论
这个分析揭示了**beginWork在 React 内部如何工作,特别是在处理 Fiber 树时它是如何区分 mount 和 update 两种不同情况的。通过复用已存在的current节点或创建新的子 Fiber 节点,beginWork使得整个渲染 过程更加高效。同时,通过灵活使用effectTag**,React 能够在 commit 阶段精确地执行 DOM 操作。
completeWork
在 React 的 Fiber 架构中,completeWork函数扮演着非常重要的角色,主要负责在“归”阶段(递归树回溯阶 段)完成各种类型的 Fiber 节点(如HostComponent,即原生 DOM 组件)的处理。它是 React 内部算法的一 部分,用于处理组件的生命周期、状态更新、以及 DOM 操作。
函数流程
completeWork函数接受当前正在处理的 Fiber 节点作为参数,并根据不同的节点类型(Fiber 的tag)执 行不同的逻辑。这里主要关注与页面渲染直接相关的**HostComponent**。
function completeWork(current, workInProgress, renderLanes) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// ...逻辑
return null;
}
// ...其他类型
}
}function completeWork(current, workInProgress, renderLanes) {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case HostComponent: {
popHostContext(workInProgress);
const rootContainerInstance = getRootHostContainer();
const type = workInProgress.type;
// ...逻辑
return null;
}
// ...其他类型
}
}处理 HostComponent
对于**HostComponent,completeWork函数分别处理节点的挂载(mount)和更新(update)。如果是更新操 作,会通过调用updateHostComponent**函数处理如回调函数、样式等 props。
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(current, workInProgress, type, newProps, rootContainerInstance);
}if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(current, workInProgress, type, newProps, rootContainerInstance);
}更新和挂载的差异
- 更新时:已有与 Fiber 节点对应的 DOM 节点,因此主要处理 props 的更新。
- 挂载时:需要生成 DOM 节点,并处理子 DOM 节点和 props。
EffectList
为了在 commit 阶段执行所有的副作用(side-effects),React 使用了**effectList。这是一个单向链表,用 于存储所有有effectTag**的 Fiber 节点。这样,在 commit 阶段只需要遍历这个链表而不需要重新遍历整个 Fiber 树。
结论
通过**completeWork**和其相关函数,React 成功地处理了 Fiber 树中的每个节点,并为 commit 阶段做好了 准备,这大大优化了渲染性能和可维护性。在 commit 阶段,所有这些准备工作都将应用到实际的 DOM 中,完成 整个更新周期。
第四章 commit 阶段
流程概览
React Fiber 架构中的 Commit 阶段是应用更新生命周期中最后一步,负责实际改变 DOM 并触发各种副作用。该 阶段可以粗略地分为三个子阶段:
- Before Mutation 阶段:执行 DOM 操作前
- Mutation 阶段:执行 DOM 操作
- Layout 阶段:执行 DOM 操作后
每个阶段都有其独特的工作。以下是详细的解释和代码示例。
Before Mutation 阶段
在进入 before mutation 阶段之前,commitRootImpl 方法主要进行变量赋值和状态重置。这里,一 个特别重要的变量是 firstEffect,这个变量会在后续的三个子阶段都被使用。
let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
firstEffect = finishedWork.firstEffect;
}let firstEffect;
if (finishedWork.effectTag > PerformedWork) {
if (finishedWork.lastEffect !== null) {
finishedWork.lastEffect.nextEffect = finishedWork;
firstEffect = finishedWork.firstEffect;
} else {
firstEffect = finishedWork;
}
} else {
firstEffect = finishedWork.firstEffect;
}Mutation 阶段
在这个阶段,实际的 DOM 操作会被执行。这里会遍历 firstEffect 链表,并根据其中每个 Fiber 节点的 updateQueue 执行对应的 DOM 更新。
Layout 阶段
Layout 阶段主要用于执行 DOM 操作后的工作。这通常包括触发一些生命周期钩子(如 componentDidUpdate)和一些 hooks(如 useLayoutEffect)。
额外工作:Before Mutation 和 Layout 之间
除了这三个主要阶段之外,还有一些额外的工作。这些通常涉及到 useEffect 的触发、优先级相关的重 置、以及 ref 的绑定和解绑等。
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
}if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
}案例:useLayoutEffect 和 useEffect 的区别
考虑一个 useLayoutEffect 示例,在这里当你点击页面上的数字时,数字会先变为 0,然后在 useLayoutEffect 回调中变为一个随机数。由于 useLayoutEffect 在 Layout 阶段同步执行,数字 将直接变为随机数,而不会先显示为 0。
这与 useEffect 不同,如果使用 useEffect,你会看到数字先变为 0,然后变为随机数。
总结
Commit 阶段是 React 更新周期中的最后一步,它负责实际更新 DOM 和触发副作用。了解这一阶段的工作原理有 助于我们更有效地使用 React,并优化应用性能。
before mutation 阶段
本文旨在概述 React 中的 commit 阶段的"before mutation"子阶段,特别关注 于commitBeforeMutationEffects函数。这个函数扮演着在 DOM 实际更改之前处理多种副作用和生命周期钩子的 角色。
commit 阶段概览
commit 阶段是 React 中 DOM 更新的最后阶段,通常分为三个子阶段:before mutation(执行 DOM 操作 前)、mutation(执行 DOM 操作)、layout(执行 DOM 操作后)。
before mutation 阶段核心内容
在before mutation阶段,commitBeforeMutationEffects函数是核心函数。该函数在这一阶段做了以下几件 事:
- 处理 DOM 节点渲染或删除后的
autoFocus、blur逻辑。 - 调用
getSnapshotBeforeUpdate生命周期钩子。 - 调度
useEffect。
commitBeforeMutationEffects 函数分解
以下是该函数的一部分代码:
javascriptCopy code
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if ((effectTag & Snapshot) !== NoEffect) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
if ((effectTag & Passive) !== NoEffect) {
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
nextEffect = nextEffect.nextEffect;
}
}javascriptCopy code
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if ((effectTag & Snapshot) !== NoEffect) {
commitBeforeMutationEffectOnFiber(current, nextEffect);
}
if ((effectTag & Passive) !== NoEffect) {
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
nextEffect = nextEffect.nextEffect;
}
}这个函数在遍历effectList时,根据effectTag来确定何时调用getSnapshotBeforeUpdate和何时调 度useEffect。
getSnapshotBeforeUpdate 生命周期钩子
该钩子是一个替代于componentWillUpdate的生命周期方法,它在 DOM 更新之前被调用。因为 commit 阶段是同 步的,所以这个钩子只会被调用一次,避免了由于异步渲染带来的问题。
调度 useEffect
在该阶段,useEffect会被异步调度:
if ((effectTag & Passive) !== NoEffect) {
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}if ((effectTag & Passive) !== NoEffect) {
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}scheduleCallback会触发flushPassiveEffects,这个函数会遍历rootWithPendingPassiveEffects来执行具 体的 effect。
为什么异步?
异步调度useEffect的原因是为了防止阻塞浏览器渲染,这与 React 官方文档的建议相符。
总结
before mutation阶段主要包括处理 DOM 节点的autoFocus和blur逻辑,调用getSnapshotBeforeUpdate生 命周期钩子,并调度useEffect。这个阶段对于 React 的功能和性能优化都是至关重要的。
mutation 阶段
你提供了一段详细的分析,主要针对 React 源码中 DOM 操作的“mutation 阶段”,概述了这个阶段如何遍 历**effectList**来应用变化。你涉及了主要的三种操作:Placement(放置)、Update(更新)和 Deletion(删除)。
Mutation 阶段简介
在 React 应用的渲染周期中,mutation 阶段是执行 DOM 更新的关键步骤。这一阶段遍历**effectList,对每 个 Fiber 节点执行相应的操作,具体取决于该 Fiber 节点的effectTag**。
核心操作
Placement(放置): 当 Fiber 节点具有 Placement effectTag,React 会调用**
commitPlacement**方 法,将该节点插入到 DOM 中。jsfunction commitPlacement(finishedWork) { const parentFiber = getHostParentFiber(finishedWork); const parentStateNode = parentFiber.stateNode; const before = getHostSibling(finishedWork); if (isContainer) { insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); } else { insertOrAppendPlacementNode(finishedWork, before, parent); } }function commitPlacement(finishedWork) { const parentFiber = getHostParentFiber(finishedWork); const parentStateNode = parentFiber.stateNode; const before = getHostSibling(finishedWork); if (isContainer) { insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent); } else { insertOrAppendPlacementNode(finishedWork, before, parent); } }Update(更新): 具有 Update effectTag 的 Fiber 节点会通过**
commitWork**进行更新。function commitWork(current, nextEffect) { //...根据Fiber.tag进行操作,可能是FunctionComponent或HostComponent等 }function commitWork(current, nextEffect) { //...根据Fiber.tag进行操作,可能是FunctionComponent或HostComponent等 }Deletion(删除): 如果 Fiber 节点有 Deletion effectTag,**
commitDeletion**方法会被调用。function commitDeletion(root, nextEffect, renderPriorityLevel) { //...执行删除操作 }function commitDeletion(root, nextEffect, renderPriorityLevel) { //...执行删除操作 }
注意点
- getHostSibling 的复杂度: 获取兄弟节点可能是复杂和耗时的,因为 Fiber 节点可能不与 DOM 节点一一 对应。
实例代码
假设我们有以下 React 组件结构:
function App() {
return (
<div>
<Item/>
</div>
);
}
function Item() {
return <li></li>;
}function App() {
return (
<div>
<Item/>
</div>
);
}
function Item() {
return <li></li>;
}当我们更新 App 组件如下:
function App() {
return (
<div>
<p></p>
<Item/>
</div>
);
}function App() {
return (
<div>
<p></p>
<Item/>
</div>
);
}这会导致 DOM 从**#root ---> div ---> li变为#root ---> div ---> p ---> li,React 会通过遍 历effectList**并使用相应的 effectTag 来完成这一更新。
总结
React 的 mutation 阶段是非常关键的,它确保了虚拟 DOM 与实际 DOM 保持同步。通 过**effectList和effectTag,React 能以高效和可预测的方式应用这些更改。注意某些操作, 如getHostSibling**,可能会增加额外的计算负担。
layout 阶段
本文主要解释了 React 的**Layout阶段,在这一阶段中,DOM 的更改已经完成,但尚未呈现。重要的是,这一 阶段可以访问和参与 DOM 布局。Layout阶段由commitLayoutEffects函数进行管理,该函数遍 历effectList,调用相应的生命周期钩子和 Hooks,并更新ref**。
commitLayoutEffects
代码实现部分如下:
function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
while (nextEffect !== null) {
const effectTag = nextEffect.effectTag;
if (effectTag & (Update | Callback)) {
const current = nextEffect.alternate;
commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
}
if (effectTag & Ref) {
commitAttachRef(nextEffect);
}
nextEffect = nextEffect.nextEffect;
}
}此函数主要做两件事:
- 调用**
commitLayoutEffectOnFiber**,负责调用与生命周期钩子和 Hooks 相关的操作。 - 调用**
commitAttachRef,负责赋值ref**。
commitLayoutEffectOnFiber
这个函数主要是根据**fiber.tag**来对不同类型的节点进行不同的处理。
- 对于**
ClassComponent,它会区分是mount还是update,然后调 用componentDidMount或componentDidUpdate**。
javascriptCopy code
this.setState({ xxx: 1 }, () => {
console.log("i am update~");
});javascriptCopy code
this.setState({ xxx: 1 }, () => {
console.log("i am update~");
});- 对于**
FunctionComponent,它会调用useLayoutEffect** hook 的回调函数。
javascriptCopy code
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
schedulePassiveEffects(finishedWork);
return;
}javascriptCopy code
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
schedulePassiveEffects(finishedWork);
return;
}这里值得注意的是,**useLayoutEffect与useEffect的主要区别在于,前者在Layout**阶段同步执行,后者 则异步执行。
commitAttachRef
此函数的主要任务是更新**ref**。
javascriptCopy code
function commitAttachRef(finishedWork: Fiber) {
// ...省略
if (typeof ref === "function") {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}javascriptCopy code
function commitAttachRef(finishedWork: Fiber) {
// ...省略
if (typeof ref === "function") {
ref(instanceToUse);
} else {
ref.current = instanceToUse;
}
}Fiber 树切换
最后,root.current = finishedWork; 这行代码是用于将**workInProgress Fiber树切换 成current Fiber**树。
这行代码的位置很重要,它确保了在**componentWillUnmount执行时,我们仍然可以访问到旧的 DOM,而 在componentDidMount和componentDidUpdate**执行时,我们可以访问到新的 DOM。
总结
Layout阶段遍历effectList,执行**commitLayoutEffects,该方法主要工作是根据effectTag调用不 同的处理函数处理 Fiber 并更新ref**。