网站首页 > 技术文章 正文
默认大家多多少少都是了解过React(^16 的版本中,React才使用 Fiber 架构来表示组件树和管理更新过程)。老规矩,废话不多说,直接 coding...
1、Reconciler 是个啥?
Reconciler 翻译过来就是协调器,它负责处理React元素的更新并在内部构建虚拟 DOM。从 JSX 到真实 UI 的过程包括以下几个关键阶段:
- JSX 编译阶段:将 JSX 编译成 React.createElement 调用。
- 创建虚拟 DOM:生成虚拟 DOM 对象。
- 创建 Fiber 树:将虚拟 DOM 转换为 Fiber 树。
- 协调(Reconciliation):比较新旧 Fiber 树,找出需要更新的部分。
- 提交阶段(Commit Phase):将所有的更新应用到真实 DOM 中。
- 渲染到真实 DOM:更新后的 DOM 反映在浏览器中。
这个过程在实际的React实现中要复杂得多,特别是在处理并发更新、调度优先级和性能优化方面。
下面是一个简化的示例代码,展示了从 JSX 到真实 UI 的过程:
// 1. JSX 语法
const element = <h1>Hello, world!</h1>;
// 2. 编译为 JavaScript
// const element = React.createElement('h1', null, 'Hello, world!');
// 3. 创建虚拟DOM
const virtualDOM = {
type: 'h1',
props: {
children: 'Hello, world!'
}
};
// 4. 创建 Fiber 树(简化)
function createFiber(node) {
return {
type: node.type,
props: node.props,
stateNode: null,
child: null,
sibling: null,
return: null
};
}
const fiberRoot = createFiber(virtualDOM);
// 5. 协调(简化)
function reconcile(fiber) {
if (!fiber.stateNode) {
// 创建真实DOM节点
fiber.stateNode = document.createElement(fiber.type);
fiber.stateNode.textContent = fiber.props.children;
}
// 递归处理子节点和兄弟节点
if (fiber.child) {
reconcile(fiber.child);
}
if (fiber.sibling) {
reconcile(fiber.sibling);
}
}
// 6. 提交阶段(简化)
function commitRoot(fiber) {
document.getElementById('root').appendChild(fiber.stateNode);
}
// 执行协调和提交
reconcile(fiberRoot);
commitRoot(fiberRoot);
2、FiberNode 函数如何实现?
FiberNode是 Reconciler 的核心数据结构之一,用于构建协调树。Reconciler 使用 FiberNode 来表示 React 元素树中的节点,并通过比较 Fiber 树的差异,找出需要进行更新的部分,生成更新指令,来实现 UI 的渲染和更新。
既然是用来表示节点的,那我们来看看它包含了哪些重要的字段:
- type:节点的类型,可以是原生 DOM 元素、函数组件或类组件等;
- stateNode:节点对应的实际 DOM 节点或组件实例;
- child:指向节点的第一个子节点;
- sibling:指向节点的下一个兄弟节点;
- return:指向节点的父节点;
- alternate:指向节点的备份节点,用于在协调过程中进行比较(用于双缓冲机制);
- pendingProps:表示节点的新属性,用于在协调过程中进行更新。
/**
* 定义 FiberNode 构造函数
* /packages/react-reconciler/src/ReactFiber.new.js
*/
function FiberNode(
tag: WorkTag, // 节点的类型(例如,函数组件、类组件、DOM元素等)
pendingProps: mixed, // 节点的新属性
key: null | string, // 节点的唯一标识符,用于优化列表渲染
mode: TypeOfMode, // 节点的模式(例如,严格模式、并发模式等)
) {
// 实例属性初始化
this.tag = tag; // 保存节点类型
this.key = key; // 保存节点的唯一标识符
this.elementType = null; // 保存节点的元素类型(用于函数组件)
this.type = null; // 保存节点的类型(例如,DOM元素的标签名或组件函数)
this.stateNode = null; // 保存与节点关联的实际DOM节点或类组件实例
// Fiber属性初始化
this.return = null; // 保存指向父Fiber节点的引用
this.child = null; // 保存指向第一个子Fiber节点的引用
this.sibling = null; // 保存指向下一个兄弟Fiber节点的引用
this.index = 0; // 保存节点在兄弟节点中的索引位置
this.ref = null; // 保存节点的ref属性,用于访问DOM节点或组件实例
this.pendingProps = pendingProps; // 保存节点的新属性,用于在协调过程中进行更新
this.memoizedProps = null; // 保存上一次渲染时的属性
this.updateQueue = null; // 保存节点的更新队列,用于管理状态更新和副作用
this.memoizedState = null; // 保存上一次渲染时的状态
this.dependencies = null; // 保存节点的依赖项(用于Context等)
this.mode = mode; // 保存节点的模式(例如,严格模式、并发模式等)
// 副作用相关属性初始化
this.flags = NoFlags; // 保存节点的副作用标志
this.subtreeFlags = NoFlags; // 保存子树的副作用标志
this.deletions = null; // 保存需要删除的子节点
this.lanes = NoLanes; // 保存节点的更新优先级
this.childLanes = NoLanes; // 保存子节点的更新优先级
this.alternate = null; // 保存节点的备用Fiber,用于双缓冲机制
// 如果启用了 Profiler 计时器
if (enableProfilerTimer) {
// 注意:以下操作是为了避免 V8 性能问题
//
// 初始化下面的字段为 double 类型,避免 Fibers 具有不同的形状。
// 这与 Object.preventExtension() 有关,
// 这种性能问题只影响开发环境,但会使 React 变得非常慢。
// 解决方法是初始化为 double 类型。
//
// 更多信息请见:
// https://github.com/facebook/react/issues/14365
// https://bugs.chromium.org/p/v8/issues/detail?id=8538
this.actualDuration = Number.NaN; // 记录实际渲染持续时间
this.actualStartTime = Number.NaN; // 记录实际渲染开始时间
this.selfBaseDuration = Number.NaN; // 记录自身基础持续时间
this.treeBaseDuration = Number.NaN; // 记录子树基础持续时间
// 可以在初始化后将 double 类型替换为其他类型,不会触发上述性能问题
// 这样可以简化其他分析器代码(包括开发者工具)
this.actualDuration = 0; // 初始化实际渲染持续时间
this.actualStartTime = -1; // 初始化实际渲染开始时间
this.selfBaseDuration = 0; // 初始化自身基础持续时间
this.treeBaseDuration = 0; // 初始化子树基础持续时间
}
// 如果在开发环境中
if (__DEV__) {
// 以下字段不直接使用,但有助于调试内部机制
this._debugSource = null; // 保存调试信息的源代码位置
this._debugOwner = null; // 保存调试信息的拥有者组件
this._debugNeedsRemount = false; // 标记是否需要重新挂载
this._debugHookTypes = null; // 保存调试信息的Hook类型
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this); // 防止扩展当前对象,帮助调试
}
}
}
3、Reconciler 的工作流程
React Fiber Reconciler
Reconciler 的工作流程总的来说就是对 Fiber 树进行一次 深度优先遍历(DFS) ,首先访问根节点,然后依次访问左子树和右子树,通过比较新节点(新生成的 React Element)和旧节点(现有的 FiberNode),生成更新计划,并打上不同的标记。
- 遍历 Fiber 树: React 使用深度优先搜索(DFS)算法来遍历 Fiber 树,首先会从 Fiber 树的根节点开始进行遍历,遍历整个组件树的结构。
- 比较新旧节点: 对于每个 Fiber 节点,Reconciler 会比较新节点(即新的 React Element)和旧节点(即现有的 FiberNode)之间的差异,比较的内容包括节点类型、属性、子节点等方面的差异。
- 生成更新计划: 根据比较的结果,Reconciler 会生成一个更新计划,用于确定需要进行的操作,更新计划通常包括哪些节点需要更新、哪些节点需要插入到 DOM 中、哪些节点需要删除等信息。
- 打标记(Tagging): 为了记录不同节点的操作,React 会为每个节点打上不同的标记。例如,如果节点需要更新,可能会打上更新标记(Update Tag);如果节点是新创建的,可能会打上插入标记(Placement Tag);如果节点被移除,可能会打上删除标记(Deletion Tag)等。
- 更新 Fiber 节点: 根据生成的更新计划和标记,Reconciler 会更新对应的 Fiber 节点以反映组件的最新状态。更新操作可能包括更新节点的状态、更新节点的属性、调用生命周期方法等。
- 递归处理子节点: 对于每个节点的子节点,React 会递归地重复进行上述的比较和更新操作,以确保整个组件树都得到了正确的处理。
当所有 React Element 都比较完成之后,会生成一棵新的 Fiber 树,此时,一共存在两棵 Fiber 树:
- current: 与视图中真实 UI 对应的 Fiber 树,当 React 开始新的一轮渲染时,会使用 current 作为参考来比较新的树与旧树的差异,决定如何更新 UI;
- workInProgress: 触发更新后,正在 Reconciler 中计算的 Fiber 树,一旦 workInProgress 上的更新完成,它将会被提交为新的 current,成为下一次渲染的参考树,并清空旧的 current 树。
下面我们来看看 Reconciler 相关函数的具体实现: 查看所有源码,猛戳 /packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// 面试官:renderRootSync和renderRootConcurrent有什么区别,各自适合什么场景,如何触发?
/**
* 并发渲染根节点
* 该函数用于处理React的并发渲染,负责调度和执行高优先级的渲染任务,
* 并在必要时处理异常。
*/
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// 保存当前的执行上下文
const prevExecutionContext = executionContext;
// 设置执行上下文为渲染上下文
executionContext |= RenderContext;
// 推入调度器,并保存之前的调度器
const prevDispatcher = pushDispatcher();
// 如果当前工作中的根节点或者渲染优先级发生了变化,需要重新准备渲染栈
if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
// 如果启用了更新跟踪且开发者工具存在
if (enableUpdaterTracking) {
if (isDevToolsPresent) {
// 获取 memoizedUpdaters(已记忆的更新器)
const memoizedUpdaters = root.memoizedUpdaters;
// 如果有未处理的更新器
if (memoizedUpdaters.size > 0) {
// 恢复挂起的更新器,并清空 memoizedUpdaters
restorePendingUpdaters(root, workInProgressRootRenderLanes);
memoizedUpdaters.clear();
}
// 将调度的 Fiber 移动到 memoized 中
movePendingFibersToMemoized(root, lanes);
}
}
// 获取渲染优先级对应的 transitions
workInProgressTransitions = getTransitionsForLanes(root, lanes);
// 重置渲染计时器
resetRenderTimer();
// 准备一个新的渲染栈
prepareFreshStack(root, lanes);
}
// 如果是开发环境,并且启用了调试追踪
if (__DEV__) {
if (enableDebugTracing) {
// 记录渲染开始的日志
logRenderStarted(lanes);
}
}
// 如果启用了调度分析器,标记渲染已开始
if (enableSchedulingProfiler) {
markRenderStarted(lanes);
}
// 开始主要的渲染工作循环
do {
try {
// 执行并发渲染的主循环
workLoopConcurrent();
// 成功执行则退出循环
break;
} catch (thrownValue) {
// 如果出现异常,处理错误
handleError(root, thrownValue);
}
} while (true);
// 重置上下文依赖关系
resetContextDependencies();
// 弹出调度器,恢复之前的调度器状态
popDispatcher(prevDispatcher);
// 恢复之前的执行上下文
executionContext = prevExecutionContext;
// 如果是开发环境,并且启用了调试追踪
if (__DEV__) {
if (enableDebugTracing) {
// 记录渲染结束的日志
logRenderStopped();
}
}
// 检查树是否已经完成渲染
if (workInProgress !== null) {
// 如果仍有工作待完成,标记渲染已暂停
if (enableSchedulingProfiler) {
markRenderYielded();
}
// 返回树仍在进行中的状态
return RootInProgress;
} else {
// 如果树已完成渲染
if (enableSchedulingProfiler) {
markRenderStopped();
}
// 清空当前工作中的根节点
workInProgressRoot = null;
workInProgressRootRenderLanes = NoLanes;
// 返回最终的退出状态
return workInProgressRootExitStatus;
}
}
/**
* 准备一个新的渲染堆栈,用于处理新的更新
* 该函数重置与上一次渲染相关的状态,并为当前根节点创建一个新的工作副本,
* 以便开始处理新的渲染任务。
*/
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
// 清空上一次渲染完成的工作和优先级
root.finishedWork = null;
root.finishedLanes = NoLanes;
// 获取根节点的超时句柄
const timeoutHandle = root.timeoutHandle;
if (timeoutHandle !== noTimeout) {
// 如果根节点之前暂停并调度了一个超时来提交回退状态
// 现在有了额外的工作,取消超时
root.timeoutHandle = noTimeout;
// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
cancelTimeout(timeoutHandle);
}
// 如果有正在进行的工作
if (workInProgress !== null) {
// 获取被中断工作的上一级节点
let interruptedWork = workInProgress.return;
// 循环处理被中断的工作
while (interruptedWork !== null) {
// 获取当前节点的副本
const current = interruptedWork.alternate;
// 处理被中断的工作
unwindInterruptedWork(
current,
interruptedWork,
workInProgressRootRenderLanes,
);
// 继续向上一级节点移动
interruptedWork = interruptedWork.return;
}
}
// 设置当前正在进行的根节点
workInProgressRoot = root;
// 创建根节点的工作副本
const rootWorkInProgress = createWorkInProgress(root.current, null);
// 设置当前正在进行的工作节点
workInProgress = rootWorkInProgress;
// 设置当前渲染优先级和其他相关状态
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootInProgress;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootInterleavedUpdatedLanes = NoLanes;
workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
workInProgressRootConcurrentErrors = null;
workInProgressRootRecoverableErrors = null;
// 完成并发更新队列
finishQueueingConcurrentUpdates();
// 如果是开发环境,丢弃待处理的严格模式警告
if (__DEV__) {
ReactStrictModeWarnings.discardPendingWarnings();
}
// 返回根节点的工作副本
return rootWorkInProgress;
}
/**
* 深度优先遍历,向下递归子节点
*/
function workLoopConcurrent() {
// 执行工作,直到调度器要求我们暂停
while (workInProgress !== null && !shouldYield()) {
// 执行一个工作单元
performUnitOfWork(workInProgress);
}
}
/**
* 该函数负责执行一个fiber节点的工作。它会根据当前的fiber状态调用beginWork,
* 如果该fiber没有生成新的工作,则调用completeUnitOfWork来完成当前的工作。
*/
function performUnitOfWork(unitOfWork: Fiber): void {
// 当前fiber的已刷新状态是alternate。理想情况下,不应依赖此状态,
// 但在这里依赖它意味着我们不需要在工作进度中添加额外的字段。
const current = unitOfWork.alternate;
setCurrentDebugFiberInDEV(unitOfWork);
let next;
// 如果启用了Profiler计时器且当前fiber处于Profile模式
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
// 启动Profiler计时器
startProfilerTimer(unitOfWork);
// 调用beginWork开始处理当前fiber
next = beginWork(current, unitOfWork, subtreeRenderLanes);
// 停止Profiler计时器并记录时间差
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
} else {
// 调用beginWork开始处理当前fiber
next = beginWork(current, unitOfWork, subtreeRenderLanes);
}
resetCurrentDebugFiberInDEV();
// 将当前fiber的待处理属性记忆到memoizedProps
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) {
// 没有子节点,则遍历兄弟节点或父节点
completeUnitOfWork(unitOfWork);
} else {
// 有子节点,继续向下深度遍历
workInProgress = next;
}
ReactCurrentOwner.current = null;
}
/**
* 该函数尝试完成当前的工作单元,然后移动到下一个兄弟fiber。
* 如果没有更多的兄弟fiber,则返回到父fiber。
*/
function completeUnitOfWork(unitOfWork: Fiber): void {
// 尝试完成当前工作单元,然后移动到下一个兄弟fiber。
// 如果没有更多兄弟fiber,则返回到父fiber。
let completedWork = unitOfWork;
do {
// 当前fiber的已刷新状态是alternate。理想情况下,不应依赖此状态,
// 但在这里依赖它意味着我们不需要在工作进度中添加额外的字段。
const current = completedWork.alternate;
const returnFiber = completedWork.return;
// 检查工作是否完成或是否有异常抛出。
if ((completedWork.flags & Incomplete) === NoFlags) {
setCurrentDebugFiberInDEV(completedWork);
let next;
// 如果未启用Profiler计时器或当前fiber不处于Profile模式
if (!enableProfilerTimer || (completedWork.mode & ProfileMode) === NoMode) {
// 完成当前工作
next = completeWork(current, completedWork, subtreeRenderLanes);
} else {
// 启动Profiler计时器
startProfilerTimer(completedWork);
// 完成当前工作
next = completeWork(current, completedWork, subtreeRenderLanes);
// 停止Profiler计时器并记录时间差
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
}
resetCurrentDebugFiberInDEV();
if (next !== null) {
// 完成当前fiber生成了新的工作,处理下一个工作单元
workInProgress = next;
return;
}
} else {
// 当前fiber未完成,因为有异常抛出。弹出栈中的值而不进入完成阶段。
// 如果这是一个边界,尽可能捕获值。
const next = unwindWork(current, completedWork, subtreeRenderLanes);
// 因为当前fiber未完成,不要重置它的lanes。
if (next !== null) {
// 如果完成当前工作生成了新的工作,处理下一个工作单元。
// 我们会再次回到这里。
// 由于我们正在重启,移除任何不是宿主效果的effect tag。
next.flags &= HostEffectMask;
workInProgress = next;
return;
}
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
// 记录发生异常的fiber的渲染持续时间。
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
// 在继续之前,包含处理失败子节点的时间。
let actualDuration = completedWork.actualDuration;
let child = completedWork.child;
while (child !== null) {
actualDuration += child.actualDuration;
child = child.sibling;
}
completedWork.actualDuration = actualDuration;
}
if (returnFiber !== null) {
// 标记父fiber为未完成并清除其子树标志。
returnFiber.flags |= Incomplete;
returnFiber.subtreeFlags = NoFlags;
returnFiber.deletions = null;
} else {
// 我们已经回到了根节点。
workInProgressRootExitStatus = RootDidNotComplete;
workInProgress = null;
return;
}
}
// 获取下一个兄弟fiber
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 如果在当前returnFiber中有更多工作要做,则处理下一个工作单元。
workInProgress = siblingFiber;
return;
}
// 否则,返回到父fiber
completedWork = returnFiber;
// 更新当前工作单元以防止异常抛出。
workInProgress = completedWork;
} while (completedWork !== null);
// 我们已经到达根节点。
if (workInProgressRootExitStatus === RootInProgress) {
workInProgressRootExitStatus = RootCompleted;
}
}
这几段代码实现了React渲染的工作循环,通过深度优先遍历Fiber树,处理和完成每个工作单元,确保 UI 的更新。
最后
有写错的,或者有什么问题,欢迎留言讨论 。
猜你喜欢
- 2024-10-01 React状态管理专题:什么是Redux(react+redux)
- 2024-10-01 Next.js 14 正式发布(next.itellyou.cn)
- 2024-10-01 React:组件的生命周期(react组件的生命周期函数)
- 2024-10-01 react native 封装一个公用的数据请带上拉分页下拉刷新的组件
- 2024-10-01 React是一个前端开发项目的JavaScript库
- 2024-10-01 「最简教程」每天一篇,轻松搞定 React——状态提升
- 2024-10-01 千万级项目后台菜单导航设计及react antd实现
- 2024-10-01 这就是你日思夜想的 React 原生动态加载
- 2024-10-01 React 渲染的未来(react 渲染流程)
- 2024-10-01 React 最简单的入门应用项目(react简单吗)
- 最近发表
- 标签列表
-
- cmd/c (64)
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- sqlset (64)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- chromepost (65)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- linux删除一个文件夹 (65)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)