🌴 Fiber 一定是当下前端面试的必问问题
搞懂这些问题不管是在面试过程还是日常开发,都会受益匪浅。
React 核心原理就是:当数据发生变化时,UI随之更新,就是所谓的数据驱动。
React 的更新分为两大阶段,分别是 Reconciliation 阶段和 Commit 阶段。
将 Reconciliation 与 Commit 分离,意味着 React DOM 和 React Native 可以使用自己的渲染器,同时共享由 React 核心提供的相同协调器(能够支持多目标)
React 16之前,协调器(Stack Reconciler)是同步的且不可中断的,这可能导致在处理大量计算密集型任务或长时间运行的任务时出现性能问题;React 16版本对这个问题进行了优化 – 引入了一种新的协调引擎(Fiber Reconciler)。
Virtual DOM 是一种编程概念,用于高效地更新和渲染用户界面。
Virtual DOM 使用 JavaScript 对象来表示真实 DOM(文档对象模型)的树状结构。在 React 中,Virtual DOM 是一个轻量级的数据结构,它模拟了真实 DOM 的结构和属性。
每当组件的状态或属性发生变化时,React 会创建一个新的 Virtual DOM 树。这个树与旧的 Virtual DOM 树进行比较,React 会计算出需要在真实 DOM 中进行的最小更改集,即上述的“协调”(Reconciliation)阶段。
整个过程:减少重排和重绘、避免不必要的 DOM 操作。
Fiber 重新实现了协调器(针对的是 Reconciliation 阶段)。它不关心渲染,尽管渲染器需要更改以支持新架构。
Fiber Reconciler 会生成一棵 Fiber 树。其是在 Virtual DOM 树的基础上增加额外的信息来生成的,它本质来说是一个链表。
const fiber = {
stateNode, // 节点实例
child, // 子节点
sibling, // 兄弟节点
return // 父节点
}
Fiber 树在首次渲染的时候会一次过生成。在后续需要 Diff 的时候,会根据已有树和最新 Virtual DOM 的信息,生成一棵新的树。这颗新树每生成一个新的节点,都会将控制权交回给主线程,去检查有没有优先级更高的任务需要执行。如果没有,则继续构建树的过程;如果有优先级更高的任务,则丢弃正在生成的树,在空闲的时候再重新执行一遍。
Fiber 是 React 16 中新的协调引擎(历经两年研究成果),旨在提高 React 应用程序的性能和响应性。
其解决了:
总之,引入了 Fiber,React 能够在不阻塞用户的正常操作下,尽可能地利用浏览器的空闲时间,进行组件的渲染更新,提供更好的用户体验。
至此,我们可以总结下上述二者的关系。
React Fiber 是 Virtual DOM 的底层实现,它提供了一种新的调度机制来处理 Virtual DOM 的更新。Fiber 引擎使得 React 能够更细粒度地控制渲染过程。
调用 React 的 render()
方法,会创建一棵由 React 元素组成的树(current Fiber)。在下一次 state 或 props 更新时,相同的 render()
方法会返回一棵不同的树(workInProgress Fiber)。React 需要基于这两棵树之间的差别来更新 UI,以保证当前 UI 与最新的树保持同步。
为了提升算法效率,React 在以下两个基础之上中提出 Diffing 算法(只对同级元素进行 Diff):
key
属性标识哪些子元素在不同的渲染中可能是不变的。复杂度:由前后两棵树完全比对的
降为 O(n)
当对比两棵树时,React 首先比较两棵树的根节点。
当根节点为不同类型的元素时,React 会销毁原有的树并且建立起新的树。
<div>
<Counter></Counter>
</div>
<p>
<Counter></Counter>
</p>
React 会销毁 Counter
组件并且重新装载一个新的组件。
当对比两个相同类型的 React 元素时,React 会保留 DOM 节点,仅比对及更新有改变的属性。
<div className="before" title="stuff" />
<div className="after" title="stuff" />
通过对比这两个元素,React 知道只需要修改 DOM 元素上的 className
属性。
当一个组件更新时,组件实例会保持不变,因此可以在不同的渲染时保持 state 一致。
默认情况下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。
<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>
,而是会重建每一个子元素。
<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 中同样存在 key1
<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
类似的处理机制还有哪些?