每次面试,你是否都听到这些问题回荡在耳边:"你对React Fiber了解多少?"、"React Fiber究竟是什么?"、"它在实际应用中有什么价值?"。别再被这些问题困扰了!React Fiber作为React的核心,已经成为前端面试的必考知识点。掌握React Fiber,不仅能让你在面试中脱颖而出,更能在实际工作中助力你优化应用性能,打造更流畅的用户体验。现在就开始深入探索React Fiber的奥秘吧,让面试和工作都变得更加轻松自如! 注:本文是基于React@v18.2.0版本
为什么使用 React Fiber?
在 React@v16
之前的版本,React 对于虚拟 DOM 是采用递归方式遍历更新的,比如一次更新,就会从应用根部递归更新,递归一旦开始,中途无法中断,随着项目越来越复杂,层级越来越深,导致更新的时间越来越长,给前端交互上的体验就是卡顿。为了解决这个问题,React就引入了Fiber架构,提高React的渲染性能和效率。它采用了一种可中断
的调和算法,并引入了优先级调度
的概念,可以更好地响应用户输入和动画变化,提高应用的流畅度和响应性。
React Fiber是什么?
在了解 React Fiber是个啥之前,你必须要知道,jsx、ReactElement、FiberNode、dom之间的关系?
- 写JSX来描述React组件的结构和内容。
- JSX被Babel转译成
React.createElement(__jsx或__jsxs)
的调用,生成ReactElement。 - 在React的协调过程中,ReactElement被转换成FiberNode。
- FiberNode是React用来进行高效渲染和更新的数据结构,它支持并发渲染和优先级调度。
- 最终,FiberNode的信息被用来更新浏览器中的真实DOM,从而呈现用户界面。
ReactElement数据结构
源代码位置 react/src/ReactElement.js
js
const element = {
$$typeof: REACT_ELEMENT_TYPE,//用来标记这个对象是一个React元素。React使用这个属性来防止XSS攻击,并确保对象是由React创建的
type: type,//div a 元素标签
key: key,
ref: ref,
props: props,
}
Fiber数据结构
源代码位置 react-reconciler/src/ReactFiber.js
ReactElement通过createFiberFromElement函数转为FiberNode
Fiber数据结构是一个链表,这样就为Fiber架构可中断渲染提供可能
js
function FiberNode(){
this.tag = tag; //元素类型
this.key = key;//元素的唯一标识。
this.elementType = null; //元素类型
this.type = null;//元素类型
this.stateNode = null;//元素实例的状态节点
// Fiber
this.return = null;//该组件实例的父级。
this.child = null;//该组件实例的第一个子级。
this.sibling = null;//该组件实例的下一个兄弟级
this.index = 0;//该组件实例在父级的子级列表中的位置。
this.ref = null;//该组件实例的ref属性
this.refCleanup = null;//ref的清理函数
this.pendingProps = pendingProps;//待处理的props(最新的)
this.memoizedProps = null;//处理后的props(上一次)
this.updateQueue = null;//TODO
this.memoizedState = null;//类组件保存state信息,函数组件保存hooks信息
this.dependencies = null;//该组件实例的依赖列表
this.mode = mode;//该组件实例的模式 (DOM模式和Canvas模式)
// Effectsx
this.flags = NoFlags$1;//副作用标签 ,之前的版本是effectTag
this.subtreeFlags= NoFlags$1;//子节点副作用标签。
this.deletions = null;//待删除的子树列表。
this.lanes = NoLanes;//任务更新的优先级区分
this.childLanes = NoLanes;//子树任务更新的优先级区分
this.alternate = null;//组件实例的备份实例,用于记录前一次更新的状态。更新时候 workInProgress会复用当前值
}
React Fiber属性繁杂?至少你在面试的时候能讲出tag、key、child、sibling、return这五点,让面试官觉得你还是懂的!
以下是这些属性的简要解释:
-
tag:
tag
用来标识Fiber节点的类型。- 不同的
tag
值代表了不同类型的React元素,比如函数组件、类组件、DOM元素等。 - React会根据
tag
的值来决定如何处理该Fiber节点。
-
key:
key
是一个可选的字符串,用于在兄弟元素之间建立唯一的身份。- 当列表重新排序或元素添加/删除时,
key
帮助React识别哪些元素发生了变化,从而高效地更新UI。 - 在Fiber节点中,
key
用于在协调过程中识别节点的身份。
-
child:
child
指向Fiber节点的第一个子节点。- 通过
child
属性,React可以遍历Fiber树,执行渲染和更新操作。
-
sibling:
sibling
指向Fiber节点的下一个兄弟节点。- 当React遍历完一个Fiber节点的所有子节点后,它会通过
sibling
属性移动到下一个兄弟节点,继续遍历。
-
return:
return
指向Fiber节点的父节点。- 通过
return
属性,React可以在Fiber树中向上回溯,这对于错误处理和优先级调度等功能非常重要。
Fiber树生成
每个React都有以下代码入口:
js
function App(){
</div>
}
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<App />,
)
源代码位置
react-dom/src/client/ReactDOMRoot.js 中createRoot函数
react-reconciler/src/ReactFiberRoot.js 中createFiberRoot、FiberRootNode函数
react-reconciler/src/ReactFiber.js 中createHostRootFiber函数
执行上面代码发生了什么?
- 首先调用createRoot方法创建FiberRoot(应用根节点)、RootFiber(Fiber树的根节点),目前对应节点上的数据都是空的,生成的数据结构如下:
js
FiberRoot={
"tag": 1,//ConcurrentRoot
"containerInfo": "div#root",//挂载的dom节点
"current": { // RooFiber
"tag": 3,//标记Fiber的类型(如类组件、函数组件、DOM组件等)
"key": null,//用于在列表或其他需要区分子元素的场景中识别Fiber的键。
"elementType": null,//通常与type相同,但在某些情况下(如懒加载组件)可能不同。它指的是要渲染的元素类型。
"type": null,//组件的类型(函数、类等)或DOM节点的类型(如'div')
"stateNode": null,//对于DOM组件,这是实际的DOM节点;对于类组件,这是组件的实例
"return": null,//指向父Fiber的指针
"child": null,//指向子Fiber的指针
"sibling": null,// 指向兄弟Fiber的指针
"index": 0,//在父Fiber的子节点列表中的索引
"ref": null,//用于获取DOM节点或类组件实例的引用
"refCleanup": null,
"pendingProps": null,//新的或待处理的props
"memoizedProps": null,//上一次渲染使用的props
"updateQueue": null,//存储状态更新和回调的队列
"memoizedState": null,//上一次渲染时的状态
"dependencies": null,
"mode": 3,//表示Fiber的渲染模式(如并发模式、阻塞模式等)
"flags": 0,//用于跟踪Fiber位字段的状态和效果的位字段
"subtreeFlags": 0,//用于跟踪Fiber子树的状态和效果的位字段
"deletions": null,//指向要删除的Fiber子树的指针
"lanes": 0,//与优先级和并发渲染相关的内部字段
"childLanes": 0,//与优先级和并发渲染相关的内部字段
"alternate": null,//在双缓冲系统中,指向对应Fiber的指针(用于新旧树之间的比较)
"actualDuration": 0,
"actualStartTime": -1,
"selfBaseDuration": 0,
"treeBaseDuration": 0,
},
//...
}
- 调用render方法创建对应Fiber节点的信息,因为上一波生成都是空,我们需要把组件App(),dev节点都构建成Fiber node。后面你就需要知道React Fiber是如何工作的?
React Fiber工作原理详解
- 双缓冲技术: React Fiber使用了类似于图形渲染中的双缓冲技术。这意味着在构建新的UI树时,React会同时在内存中维护两棵树:当前屏幕上显示的树(current tree)和正在构建的树(work-in-progress tree)。只有当新的树完全构建完成后,它才会被一次性地渲染到屏幕上,从而实现更加流畅的用户体验。
给你举个例子吧: 你正在观看一场魔术表演。魔术师在舞台上放置了一个黑色的幕布(这可以看作是后台缓冲区),然后在幕布后面进行各种准备和操作(这相当于在后台缓冲区中进行绘制操作)。观众无法看到幕布后面的情况,只能等待魔术师准备好并拉开幕布。 当魔术师完成所有的准备后,他会迅速地将幕布拉开,展示给观众一个完整的、令人惊叹的魔术效果(这相当于将后台缓冲区的内容一次性复制到前台显示设备上)。在这个过程中,观众并没有看到魔术师在幕布后面忙碌的过程,而是直接看到了最终的魔术效果。
- 任务调度: React Fiber引入了任务调度的概念,允许将渲染工作拆分成多个较小的任务单元。这些任务单元可以被中断和恢复,从而实现并发渲染。React根据任务的优先级来决定它们的执行顺序,确保高优先级的任务(如用户交互)能够优先执行。
运行方式(这里太多东西了,根本讲不完)
- Reconciliation阶段: 当React决定要更新UI时,它会启动reconciliation(协调)过程。在这个阶段,React会比较新旧两棵树之间的差异,并为需要更新的组件生成相应的Fiber节点。这个过程是异步的,可以被中断和恢复。
- Commit阶段: 当所有的Fiber节点都被处理完毕后,React会进入commit阶段。在这个阶段,React会将之前在render阶段计算出的所有变化一次性应用到DOM上,并触发相关的生命周期方法(如useEffect,useLayoutEffect方法)。这个过程是不可中断的,因为它涉及到实际的DOM操作。
- 优先级调度: React Fiber通过优先级调度来管理任务的执行顺序。每个Fiber节点都有一个与之关联的优先级,React会根据节点的优先级来决定哪些节点需要先更新。高优先级的任务(如用户交互)会打断低优先级的任务(如定时器回调)并优先执行,从而实现更流畅的用户体验。
Reconciliation阶段
在Reconciliation阶段,React会遍历Fiber树,并执行每个Fiber节点的更新逻辑。这个过程可以被分为两个阶段:beginWork和completeWork。在beginWork阶段,React会执行组件的渲染逻辑,并计算副作用(side effects)。在completeWork阶段,是向上归并的过程,如果有兄弟节点,会返回 sibling兄弟,没有返回 return 父级,一直返回到 fiebrRoot ,期间可以形成effectList,对于初始化流程会创建 DOM ,对于 DOM 元素进行事件收集,处理style,className等,这个阶段并不直接更新DOM或触发任何用户可见的更改,而是为后续的Commit阶段做准备。
beginWork做了什么?
- 对于组件,执行部分生命周期,执行 render ,得到最新的 children 。
- 向下遍历调和 children ,复用 oldFiber ( diff 算法),diff 流程。
- 打不同的副作用标签 effectTag ,比如类组件的生命周期,或者元素的增加,删除,更新 你可以看下面代码
Commit阶段
在Commit阶段,React将根据在Reconciliation阶段生成的更新计划来执行实际的DOM更新。这个过程包括更新DOM节点、处理生命周期方法(如在类组件中的useEffect
)以及执行其他与渲染相关的副作用。此阶段是同步执行的,意味着一旦开始,就会一口气完成,不会被其他任务打断。
以下是我看源代码流程图: