React 相关知识(2024)二
6. 对Fiber架构的理解 #
Fiber 即是React新的调度算法
在数据更新时,react生成了一棵更大的虚拟dom树,给第二步的diff带来了很大压力——我们想找到真正变化的部分,这需要花费更长的时间。js占据主线程去做比较,渲染线程便无法做其他工作,用户的交互得不到响应,所以便出现了react fiber。
React 为了解决这个问题,根据浏览器的每一帧执行的特性,构思出了 Fiber 来将一次任务拆解成单元,以划分时间片的方式,按照Fiber的自己的调度方法,根据任务单元优先级,分批处理或吊起任务,将一次更新分散在多次时间片中,另外, 在浏览器空闲的时候, 也可以继续去执行未完成的任务, 充分利用浏览器每一帧的工作特性。
一次更新任务是分时间片执行的,直至完成某次更新。
这样 React 更新任务就只能在规定时间内占用浏览器线程了, 如果说在这个时候用户有和浏览器的页面交互,浏览器也是可以及时获取到交互内容。
Fiber架构出现之前 react 存在的问题
- 主线程阻塞:在React 15及更早版本中,当组件树发生更新时,React会通过递归算法一次性完成整个组件树的渲染过程,这个过程如果涉及大量组件,会导致主线程长时间阻塞,无法处理其他的UI交互,从而造成卡顿和延迟,降低用户体验。
- 无法中断与恢复渲染:原有的渲染过程不具备中断和恢复的能力,一旦开始渲染,就必须等到整个过程结束,即使在中间有更高优先级的任务也需要等待。
- 无法实现增量渲染:以往的React无法有效区分渲染任务的重要性和紧急程度,所有更新任务都被视为同等重要的,无法做到逐步、增量地渲染UI。
- 资源优化不足:旧版React无法根据应用的具体需求动态分配资源,无法高效利用有限的CPU周期来优化渲染性能。
jsx 形成 dom 的以下几个步骤,react 在虚拟 dom 之前做了一层数据结构设计将ReactElement 转换为 fiberNode
![2024-07-01 10.03.20.png](https://cdn.wekic.com/_blog/1719799408203-截屏2024-07-01 10.03.20.png)
- 写JSX来描述React组件的结构和内容。
- JSX被Babel转译成React.createElement(__jsx或__jsxs)的调用,生成ReactElement。
- 在React的协调过程中,ReactElement被转换成FiberNode。
- FiberNode是React用来进行高效渲染和更新的数据结构,它支持并发渲染和优先级调度。
- 最终,FiberNode的信息被用来更新浏览器中的真实DOM,从而呈现用户界面。
Fiber数据结构是一个链表,这样就为Fiber架构可中断渲染提供可能
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会复用当前值
}
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会复用当前值
}
- 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树生成
- 首先调用createRoot方法创建FiberRoot(应用根节点)、RootFiber(Fiber树的根节点),目前对应节点上的数据都是空的,生成的数据结构如下:
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,
},
//...
}
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阶段做准备。
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)以及执行其他与渲染相关的副作用。此阶段是同步执行的,意味着一旦开始,就会一口气完成,不会被其他任务打断
- 对于组件,执行部分生命周期,执行 render ,得到最新的 children 。
- 向下遍历调和 children ,复用 oldFiber ( diff 算法),diff 流程。
- 打不同的副作用标签 effectTag ,比如类组件的生命周期,或者元素的增加,删除,更新
![截屏2024-07-01 10.04.05.png](https://cdn.wekic.com/_blog/1719799452762-截屏2024-07-01 10.04.05.png)
那 React Fiber 是怎么实现的
主要是通过两个原生的 API 来实现的 requestAnimationFrame 和 requestIdleCallback
显示器每秒 60 帧我们看着才不会感觉到卡嘛,比如动画的时候,一帧的时间内布局和绘制结束,还有剩余时间,JS 就会拿到主线程使用权,如果 JS 某个任务执行过长,动画下一帧开始时 JS 还没有执行完,就会导致掉帧,出现卡顿。
所以就通过把 JS 任务分成更小的任务块,分到每一帧上的方式,一帧时间到先暂停 JS 执行,然后下一帧绘制任完成再把主线程交给 JS,在每一帧绘制之前调用 requestAnimationFrame;在每一帧空间阶段,就是一帧动画任务完成,下一帧还没到开始时间,这中间还有时间的话就调用 requetIdleCallback,执行它里面的任务
Fiber具体都做了什么?
React Fiber架构是React在16版本推出的一种全新的、革命性的调度算法和组件更新机制。它对React核心算法进行了重构,以满足更流畅的UI渲染、更灵活的任务调度以及更好的交互体验。
react fiber使得diff阶段有了被保存工作进度的能力
Fiber架构通过引入可中断和恢复的渲染机制,以及基于任务优先级的调度系统,使得React能够更灵活地管理渲染任务,实现增量渲染和精细化调度,从而显著提升了性能和用户体验。此外,Fiber架构也为React后期支持并发渲染和异步数据流等功能打下了基础
核心特点与优势:
- 精细化任务调度:
-
- 在Fiber架构之前,React的更新是同步且整体的,一旦开始更新,直到结束才会释放浏览器主线程,这可能导致长时间阻塞,无法响应用户的其他交互。
- Fiber架构则采用了增量(Incremental)和可暂停(Interruptible)的渲染方式,将渲染任务分解为一个个小的工作单元(Fiber节点),并可以根据优先级和浏览器环境的空闲时间灵活调度,确保UI的渲染不会阻塞用户交互。
- 可恢复更新:
-
- Fiber架构允许React在渲染过程中暂停和恢复,这意味着React可以在执行更新的过程中临时切换去处理高优先级的任务(如响应用户的鼠标点击或滚动事件),然后再回到之前的渲染任务,这样就保证了用户界面始终能够快速响应用户的操作。
- 优先级调度:
-
- Fiber架构支持任务优先级排序,不同的更新任务可以根据其影响范围、交互重要性等因素赋予不同优先级,优先处理对用户体验影响更大的更新。
- 协调(Reconciliation)算法优化:
-
- Fiber架构改进了React的协调算法,使其在对比新老虚拟DOM树时更为智能,能够更快地找出需要更新的DOM节点,从而提高渲染性能。
- 并发模式与Suspense:
-
- 基于Fiber架构,React得以实现并发模式,通过时间切片(Time Slicing)技术分配渲染任务,未来还可以更好地处理异步数据加载,如Suspense组件。
总之,React Fiber架构在很大程度上提升了React的性能表现和用户体验,使得React能够在更复杂的场景下仍然保持高效、流畅和可预测的行为。同时,也为React未来的演进提供了坚实的基础和更多可能性。
底层原理
我们要找到前后状态变化的部分,必须把所有节点遍历。
在老的架构中,节点以树的形式被组织起来:每个节点上有多个指针指向子节点。要找到两棵树的变化部分,最容易想到的办法就是深度优先遍历,规则如下:
- 从根节点开始,依次遍历该节点的所有子节点;
- 当一个节点的所有子节点遍历完成,才认为该节点遍历完成;
如果你系统学习过数据结构,应该很快就能反应过来,这不过是深度优先遍历的后续遍历。根据这个规则,在图中标出了节点完成遍历的顺序。
这种遍历有一个特点,必须一次性完成。假设遍历发生了中断,虽然可以保留当下进行中节点的索引,下次继续时,我们的确可以继续遍历该节点下面的所有子节点,但是没有办法找到其父节点——因为每个节点只有其子节点的指向。断点没有办法恢复,只能从头再来一遍。
以该树为例:
在遍历到节点2时发生了中断,我们保存对节点2的索引,下次恢复时可以把它下面的3、4节点遍历到,但是却无法找回5、6、7、8节点。
在新的架构中,每个节点有三个指针:分别指向第一个子节点、下一个兄弟节点、父节点。这种数据结构就是fiber,它的遍历规则如下:
- 从根节点开始,依次遍历该节点的子节点、兄弟节点,如果两者都遍历了,则回到它的父节点;
- 当一个节点的所有子节点遍历完成,才认为该节点遍历完成;
根据这个规则,同样在图中标出了节点遍历完成的顺序。跟树结构对比会发现,虽然数据结构不同,但是节点的遍历开始和完成顺序一模一样。不同的是,当遍历发生中断时,只要保留下当前节点的索引,断点是可以恢复的——因为每个节点都保持着对其父节点的索引。
同样在遍历到节点2时中断,fiber结构使得剩下的所有节点依旧能全部被走到。
这就是react fiber的渲染可以被中断的原因。树和fiber虽然看起来很像,但本质上来说,一个是树,一个是链表。
fiber是纤程
这种数据结构之所以被叫做fiber,因为fiber的翻译是纤程,它被认为是协程的一种实现形式。协程是比线程更小的调度单位:它的开启、暂停可以被程序员所控制。具体来说,react fiber是通过requestIdleCallback这个api去控制的组件渲染的“进度条”。
requesetIdleCallback是一个属于宏任务的回调,就像setTimeout一样。不同的是,setTimeout的执行时机由我们传入的回调时间去控制,requesetIdleCallback是受屏幕的刷新率去控制。本文不对这部分做深入探讨,只需要知道它每隔16ms会被调用一次,它的回调函数可以获取本次可以执行的时间,每一个16ms除了requesetIdleCallback的回调之外,还有其他工作,所以能使用的时间是不确定的,但只要时间到了,就会停下节点的遍历。
使用方法如下:
const workLoop = (deadLine) => {
let shouldYield = false;// 是否该让出线程
while(!shouldYield){
console.log('working')
// 遍历节点等工作
shouldYield = deadLine.timeRemaining()<1;
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop);
const workLoop = (deadLine) => {
let shouldYield = false;// 是否该让出线程
while(!shouldYield){
console.log('working')
// 遍历节点等工作
shouldYield = deadLine.timeRemaining()<1;
}
requestIdleCallback(workLoop)
}
requestIdleCallback(workLoop);
requestIdleCallback的回调函数可以通过传入的参数deadLine.timeRemaining()检查当下还有多少时间供自己使用。上面的demo也是react fiber工作的伪代码。
但由于兼容性不好,加上该回调函数被调用的频率太低,react实际使用的是一个polyfill(自己实现的api),而不是requestIdleCallback。
现在,可以总结一下了:React Fiber是React 16提出的一种更新机制,使用链表取代了树,将虚拟dom连接,使得组件更新的流程可以被中断恢复;它把组件渲染的工作分片,到时会主动让出渲染主线程。
react fiber带来的变化
- 使用新架构后,动画变得流畅,宽度的变化不会卡顿;
- 使用新架构后,用户响应变快,鼠标悬停时颜色变化更快;
动画变流畅的根本原因,一定是一秒内可以获得更多动画帧。但是当我们使用react fiber时,并没有减少更新所需要的总时间。
上面是使用旧的react时,获得每一帧的时间点,下面是使用fiber架构时,获得每一帧的时间点,因为组件渲染被分片,完成一帧更新的时间点反而被推后了,我们把一些时间片去处理用户响应了。
这里要注意,不会出现“一次组件渲染没有完成,页面部分渲染更新”的情况,react会保证每次更新都是完整的。
但页面的动画确实变得流畅了,这是为什么呢?
我们现在已经知道了react fiber是在弥补更新时“无脑”刷新,不够精确带来的缺陷。这是不是能说明react性能更差呢?
并不是。孰优孰劣是一个很有争议的话题,在此不做评价。因为vue实现精准更新也是有代价的,一方面是需要给每一个组件配置一个“监视器”,管理着视图的依赖收集和数据更新时的发布通知,这对性能同样是有消耗的;另一方面vue能实现依赖收集得益于它的模版语法,实现静态编译,这是使用更灵活的JSX语法的react做不到的。
在react fiber出现之前,react也提供了PureComponent、shouldComponentUpdate、useMemo,useCallback等方法给我们,来声明哪些是不需要连带更新子组件。
- react因为先天的不足——无法精确更新,所以需要react fiber把组件渲染工作切片;而vue基于数据劫持,更新粒度很小,没有这个压力;
- react fiber这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;
7. diff的原理 #
React中的diff算法是其核心优化策略之一,用于比较新旧两个虚拟DOM树之间的差异,并找出最小化的DOM操作集以更新真实DOM。以下是React中diff算法的大致步骤概述:
- 树结构比较:
-
- React并不会简单地递归遍历整颗新旧虚拟DOM树进行全量比较,而是采用分层比较的思想,仅比较同层级的节点。
- 它首先会比较树的根节点,如果根节点不同,则直接替换整个根节点对应的真实DOM元素。
- 同层级节点比较:
-
- 对于同层级的子节点,React会尝试找到旧树中与新树中每一个节点相匹配的节点,主要依据是它们的
**key**
属性。
- 对于同层级的子节点,React会尝试找到旧树中与新树中每一个节点相匹配的节点,主要依据是它们的
-
-
- 如果找到了具有相同
key
的节点,则认为它们是同一个“实体”,只需更新属性或内容。 - 如果没有找到相同
key
的节点,则视为新增或删除节点。
- 如果找到了具有相同
-
- 属性比较与更新:
-
- 对于找到匹配节点的情况,React会进一步比较它们的属性是否有变化,如果有变化则更新相应的真实DOM元素的属性。
- 文本节点与组件节点处理:
-
- 文本节点的比较相对简单,直接比较文本内容即可。
- 组件节点则根据组件类型(类或函数组件)以及props的变化决定是否需要重新渲染。
- 子节点递归比较:
-
- 当节点匹配成功后,继续对其子节点进行同样的分层级比较,直至所有子节点都已比较完毕。
- 跳过跨层级操作:
-
- React假设在一次更新中,一个节点及其子节点不会随意地在DOM树中跨层级移动。因此,它在寻找匹配节点时只会对兄弟节点进行比较,而不是在整个树中搜索,大大降低了算法复杂度。
- 删除与插入操作:
-
- 在比较过程中,React收集到的所有需要删除的旧节点和需要插入的新节点,会在比较结束后统一进行DOM操作,避免频繁地增删DOM元素。
- 优化策略:
-
- React还有一系列优化策略,例如先处理移动或更新的节点,尽量减少DOM元素的移动次数;对组件类型不同的节点直接进行替换,不尝试深入比较等。
通过以上步骤,React的diff算法能够在O(n)的时间复杂度内完成虚拟DOM树的比较,从而实现在大量DOM更新时依然保持较高的性能。
8. 提高组件的渲染效率的?避免不必要的render? #
在React中提高组件渲染效率并避免不必要的渲染主要有以下几个策略:
- 使用PureComponent或React.memo:
-
React.PureComponent
自动进行浅比较(shallow comparison
),只有当props或state发生改变时才会触发组件重新渲染。继承自React.PureComponent
的组件会默认检查props和state对象是否严格相等。- 对于函数组件,可以使用
React.memo
对其进行包裹,React.memo
同样会进行浅比较,只有当props发生变化时才会重新渲染组件。
- 自定义shouldComponentUpdate生命周期方法:
-
- 在class组件中,可以覆盖
shouldComponentUpdate(nextProps, nextState)
方法,根据传入的新props和新state判断是否有必要调用render
方法,从而避免不必要的渲染。
- 在class组件中,可以覆盖
- 使用React Hooks进行优化:
-
- 使用
React.useState
和React.useReducer
时,可以根据业务逻辑精确控制state的变化,避免不必要的状态更新。 - 使用
React.useMemo
来缓存计算结果,仅当依赖的props或state改变时才重新计算。 - 使用
React.useCallback
来缓存函数引用,避免在props没变的情况下因为回调函数引用变了而导致不必要的子组件重渲染。
- 使用
- 优化数据结构:
-
- 避免在props或state中传递深度嵌套的对象或数组,因为React的默认浅比较无法检测到深层数据的变化。若必须使用复杂数据结构,应当在适当的地方使用
shouldComponentUpdate
、useMemo
或手动进行深比较。
- 避免在props或state中传递深度嵌套的对象或数组,因为React的默认浅比较无法检测到深层数据的变化。若必须使用复杂数据结构,应当在适当的地方使用
- 优化事件处理器:
-
- 将事件处理器封装在
useCallback
中,保证其在props不变时引用始终一致,避免无意义的组件重渲染。
- 将事件处理器封装在
- 减少不必要的state和props更新:
-
- 只有当数据实际变化时才更新state,避免频繁调用
setState
。 - 使用Context API或者Redux等状态管理库时,确保只在数据变化时触发全局状态更新。
- 只有当数据实际变化时才更新state,避免频繁调用
通过以上这些方法,可以最大程度地减少不必要的组件渲染,从而提升React应用的性能。在实践中,需要根据组件的具体情况进行权衡和选择最适合的优化方案。
9. React render方法的原理 #
React的render
方法是React组件的核心方法之一,它的基本原理和作用在于将组件的状态和属性转化为可以在浏览器中渲染的虚拟DOM表示,然后将这个虚拟DOM转化为实际的DOM操作,最终更新到浏览器的真实DOM中。
以下是render
方法的基本原理和过程:
- 首次渲染:
当组件实例化并初次插入到DOM中时,React会调用该组件的render
方法。这个方法必须返回一个React元素(可以是原生DOM元素、组件元素或Fragment),React会根据这个返回值创建一个虚拟DOM树。 - 虚拟DOM的创建与比对:
-
render
方法返回的是一个虚拟DOM树,这是一个轻量级的JavaScript对象结构,与实际的DOM树结构相似但并非真实的DOM节点。- 当组件的
props
或state
发生变化时,React会重新执行render
方法生成新的虚拟DOM树。 - React使用其内部的高效算法——虚拟DOM Diff算法,比较新旧两棵虚拟DOM树的差异。
- 最小化DOM操作:
-
- 根据虚拟DOM的比较结果,React确定最少必要的DOM操作,如添加、更新或删除DOM节点,而不是每次都完全重建DOM树。
- 这种增量更新机制极大提高了React应用的性能,因为它避免了频繁地直接操作DOM带来的性能损耗。
- DOM更新:
-
- 最终,React将这些最小化的DOM操作应用到实际DOM树上,确保用户界面得到准确、高效的更新。
React render
方法的作用就是将组件的状态和属性转化为虚拟DOM,通过虚拟DOM Diff算法来决定实际DOM的最小更新操作,从而实现高性能的用户界面更新。在类组件中,**render**
方法是必需的,而在函数组件中,函数体本身充当了**render**
方法的角色。
版权属于: vincent
转载时须注明出处及本声明