🌴 Fiber 一定是当下前端面试的必问问题
- 什么是 React Fiber?(聊背景)
- 其和虚拟 DOM 有什么关系?(谈优势)
- Diffing 算法是怎样的?(挖原理)
- ...
搞懂这些问题不管是在面试过程还是日常开发,都会受益匪浅。
说在前面
React 核心原理就是:当数据发生变化时,UI随之更新,就是所谓的数据驱动。
React 的更新分为两大阶段,分别是 Reconciliation 阶段和 Commit 阶段。
React更新 Commit
更新阶段 Reconciliation
协调阶段
- Reconciliation 阶段:React 会将新旧Virtual DOM进行对比,生成最小更新内容;
- Commit 阶段:React 会将 Reconciliation 阶段生成的更新内容应用到实际的 DOM 上,这个阶段是不能被打断的,需要一次性完成。
将 Reconciliation 与 Commit 分离,意味着 React DOM 和 React Native 可以使用自己的渲染器,同时共享由 React 核心提供的相同协调器(能够支持多目标)
React 16之前,协调器(Stack Reconciler)是同步的且不可中断的,这可能导致在处理大量计算密集型任务或长时间运行的任务时出现性能问题;React 16版本对这个问题进行了优化 -- 引入了一种新的协调引擎(Fiber Reconciler)。
Virtual DOM
Virtual DOM 是一种编程概念,用于高效地更新和渲染用户界面。
Virtual DOM 使用 JavaScript 对象来表示真实 DOM(文档对象模型)的树状结构。在 React 中,Virtual DOM 是一个轻量级的数据结构,它模拟了真实 DOM 的结构和属性。
每当组件的状态或属性发生变化时,React 会创建一个新的 Virtual DOM 树。这个树与旧的 Virtual DOM 树进行比较,React 会计算出需要在真实 DOM 中进行的最小更改集,即上述的"协调"(Reconciliation)阶段。
- 性能优化 :直接操作真实 DOM 是昂贵的,因为它会触发浏览器的重排(reflow)和重绘(repaint)。通过使用 Virtual DOM,React 可以在内存中进行变化的比较&计算,然后再将最小的更新应用到真实 DOM,从而减少不必要的 DOM 操作。
- 跨平台渲染:Virtual DOM 允许 React 在不同平台(如 Web、移动设备、虚拟现实等)上渲染 UI,因为 Virtual DOM 是与平台无关的。
- 简化开发:开发者可以像操作 JavaScript 对象一样操作 UI,而不需要关心底层的 DOM 操作。
组件状态更新 生成新的 Virtual DOM 差异比较 Diffing 更新真实 DOM
整个过程:减少重排和重绘 、避免不必要的 DOM 操作。
React Fiber
Fiber 重新实现了协调器(针对的是 Reconciliation 阶段)。它不关心渲染,尽管渲染器需要更改以支持新架构。
Fiber Reconciler 会生成一棵 Fiber 树。其是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。
javascript
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return // 父节点
}
Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程;如果有优先级更高的任务,则丢弃正在生成的树,在空闲的时候再重新执行一遍。
Fiber 是 React 16 中新的协调引擎(历经两年研究成果),旨在提高 React 应用程序的性能和响应性。
其解决了:
- 优先级 :在 Fiber 中,React 可以根据组件的重要性分配不同的更新优先级。如,用户界面中某些部分的更新可能比其他部分更紧急。
- synchronous,与之前的Stack Reconciler操作一样,同步执行
- task,在next tick之前执行
- animation,下一帧之前执行
- high,在不久的将来立即执行
- low,稍微延迟执行也没关系
- offscreen,下一次render时或scroll时才执行
- 异步更新 :在旧的协调引擎中,所有的更新都是同步进行的,这可能导致长时间的渲染阻塞,从而影响性能。React Fiber 允许更新以一种可中断的方式进行,这意味着在渲染过程中,React 可以响应其他更高优先级的任务,如,用户输入。
- 时间切片 :Fiber 引擎可以将长时间的渲染任务分割成"小块",然后在不同的时间点执行,从而避免长时间的渲染阻塞,增量渲染。
总之,引入了 Fiber,React 能够在不阻塞用户的正常操作下,尽可能地利用浏览器的空闲时间,进行组件的渲染更新,提供更好的用户体验。
至此,我们可以总结下上述二者的关系。
Fiber 与 Virtual DOM 关系
React Fiber 是 Virtual DOM 的底层实现,它提供了一种新的调度机制来处理 Virtual DOM 的更新。Fiber 引擎使得 React 能够更细粒度地控制渲染过程。
- Virtual DOM:主要是一种优化技术,用于减少实际 DOM 操作的次数及范围,提高性能;
- Fiber:是一种新的协调引擎,旨在改善 React 的渲染性能、实现更好的并发控制,并支持增量渲染,从而提高 React 的响应能力。
基于 Fiber 的 Diffing 算法
调用 React 的 render()
方法,会创建一棵由 React 元素组成的树(current Fiber)。在下一次 state 或 props 更新时,相同的 render()
方法会返回一棵不同的树(workInProgress Fiber)。React 需要基于这两棵树之间的差别来更新 UI,以保证当前 UI 与最新的树保持同步。
为了提升算法效率,React 在以下两个基础之上中提出 Diffing 算法(只对同级元素进行 Diff):
- 两个不同类型的元素会产生出不同的树;
- 开发者可以使用
key
属性标识哪些子元素在不同的渲染中可能是不变的。
复杂度:由前后两棵树完全比对的 O ( n 3 ) O(n^3) O(n3) 降为 O(n)
节点类型不同 否 是 是 否 否 是 创建新节点 销毁原节点 开始 同类型
节点 结束 key相同 复用 属性
变化 递归子节点 修改属性
当对比两棵树时,React 首先比较两棵树的根节点。
对比不同类型的元素
当根节点为不同类型的元素时,React 会销毁原有的树并且建立起新的树。
html
<div>
<Counter></Counter>
</div>
<p>
<Counter></Counter>
</p>
React 会销毁 Counter
组件并且重新装载一个新的组件。
对比同一类型的元素/组件
当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
html
<div className="before" title="stuff" />
<div className="after" title="stuff" />
通过对比这两个元素,React 知道只需要修改 DOM 元素上的 className
属性。
当一个组件更新时,组件实例会保持不变,因此可以在不同的渲染时保持 state 一致。
对子节点进行递归
默认情况下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。
html
<ul>
<li>first</li>
<li>second</li>
</ul>
<ul>
<li>first</li>
<li>second</li>
<li>third</li>
</ul>
React 会先匹配两个 <li>first</li>
对应的树,然后匹配第二个元素 <li>second</li>
对应的树,最后插入第三个元素的 <li>third</li>
树。在子元素列表末尾新增元素时,更新开销比较小。
但对于下述情况,React 并不会意识到应该保留 <li>Duke</li>
和 <li>Villanova</li>
,而是会重建每一个子元素。
html
<ul>
<li>Duke</li>
<li>Villanova</li>
</ul>
<ul>
<li>Connecticut</li>
<li>Duke</li>
<li>Villanova</li>
</ul>
这种情况会带来性能问题。
为了解决上述问题,React 引入了 key
属性。当子元素拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素。
Vue 中同样存在 key^1^
html
<ul>
<li key="Duke">Duke</li>
<li key="Villanova">Villanova</li>
</ul>
<ul>
<li key="Connecticut">Connecticut</li>
<li key="Duke">Duke</li>
<li key="Villanova">Villanova</li>
</ul>
现在 React 知道只有带着 'Connecticut'
key 的元素是新元素,带着 'Duke'
以及 'Villanova'
key 的元素仅仅移动了。
key
列表中需要保持唯一,也可以使用元素在数组中的下标作为 key,但需要注意可能导致相关问题。^2^
扩展
类似的处理机制还有哪些?
- Eventloop^3^: JavaScript 运行时的一种机制,它负责处理异步任务和回调函数。在浏览器中,Eventloop 允许 JavaScript 代码在执行过程中响应用户输入、网络请求等事件,而不会因为长时间的计算或渲染任务而变得无响应。Event Loop 通过任务队列(Task Queue)和微任务队列(Microtask Queue)来管理这些任务的执行顺序。
- CPU切片法:是一种让多个进程或线程共享同一台机器的CPU资源的方法,每个进程被分配一个时间段,称为时间片(Time Quantum),在这个时间段内,进程可以执行其任务。当时间片用完时,CPU 控制权会转移到下一个进程。其提高了计算机系统的整体效率及用户体验。
-
https://blog.csdn.net/ligang2585116/article/details/131761866揭秘Vue核心】为什么不建议在 v-for 指令中使用 index 作为 key,让你秒懂! ↩︎
-
https://zh-hans.legacy.reactjs.org/redirect-to-codepen/reconciliation/index-used-as-key 使用下标作为 key 时导致的问题 ↩︎
-
https://blog.csdn.net/ligang2585116/article/details/98023768 Event loop及macrotask & microtask ↩ ↩︎