hawx1993 / tech-blog

📦My personal tech blog,not regularly update
http://sf.gg/u/trigkit4/articles
339 stars 30 forks source link

深入理解React Fiber机制 #32

Open hawx1993 opened 1 year ago

hawx1993 commented 1 year ago

引言

在深入了解FIber之前,我们可以先了解一下React的架构发展历程:

React15架构可以分为两层:

React16架构可以分为三层:

React 官方为什么推出Fiber

React开发了Fiber架构是为了解决React中的某些性能问题,并提供一些新的功能。

其中一个主要的目的是解决React的"单线程渲染"的问题。在React的传统架构中,整个渲染过程都在单个JavaScript线程上进行,这意味着如果组件树中有很多大型组件,那么在渲染过程中可能会发生长时间的阻塞。这可能会导致性能问题,并且可能会对用户界面产生不良影响。

Fiber架构通过使用协程来解决这个问题。协程允许在渲染过程中暂停和重新启动组件的渲染,这使得可以优先处理优先级较高的组件,从而提高性能。

此外,Fiber架构还提供了许多其他新功能,包括更好的动画支持、更好的浏览器事件处理、更好的同步和异步渲染支持以及更好的可访问性支持。

题外话:Fiber架构的协程是什么

Fiber架构使用协程来优化React应用程序的渲染过程。在Fiber架构中,每个组件都对应一个协程,并且可以在渲染过程中暂停和重新启动组件的渲染。这使得可以优先处理优先级较高的组件,从而提高性能。

Fiber架构的协程是通过使用JavaScript的Generator函数来实现的

Generator函数是一种特殊的函数,可以在执行时暂停并保存当前的执行状态。这使得可以在执行过程中随时暂停函数,并在稍后恢复执行。在Fiber架构中,每个组件都对应一个Generator函数,该函数在渲染过程中被调用,并在必要时暂停和恢复执行。

需要注意的是,Fiber架构的协程并不是真正的多线程协程,而是在单个JavaScript线程上模拟多线程的行为。然而,它可以在不创建新线程的情况下实现类似于多线程的效果,并且在某些情况下可以提高性能。

那么,Fiber 是什么?

React Fiber 可以认为是 React 核心算法的重新实现。那么如何理解React的中的Fiber呢? 我们可以从两个层面来理解:

题外话:链表长啥样

image.png

如上图,其中memoizedState就是我们存放hooks数据的地方。它是一个通过 next 串联的链表。

那么,Fiber主要用于解决什么问题呢?

Fiber解决了什么问题?

在React15及以前,采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。

我们知道,浏览器分配给 js是单线程 的,JS和UI线程是互斥的,每当JS线程执行,UI线程会被挂起,等待JS执行完成后,再继续。因此用户会感知到页面卡顿情况。

为此,React Fiber 引入了动态优先级和可中断渲染:

通过解决这些问题,React Fiber可以提高 UI 渲染的性能,特别是在处理大型或复杂的 UI 时。它还为React引入了许多新功能,例如动画优化和异步渲染。

那么,什么是动态优先级呢?

动态优先级是React Fiber引入的一种新的概念,允许React在"reconciliation"过程中根据当前状态调整工作的优先级。这可以通过调整每个任务的"权重"来实现。

在React Fiber中,每个任务都有一个权重值,表示它的优先级。例如,一个任务的权重可能比另一个任务的权重高,这意味着它会在后者之前进行。权重值可以是正数或负数,越大的权重值表示越高的优先级。

React Fiber使用这些权重值来决定哪些任务应该更早执行,从而提高性能。例如,如果有一个长时间运行的任务,它可以设置为较低的权重值,以便其他任务可以先完成。这可以使浏览器更快地响应用户输入,提高用户体验。

动态优先级是React Fiber通过跟踪当前状态并不断调整优先级来实现的。它可以通过React的"scheduleUpdate"方法来控制,该方法允许开发人员为特定的组件设置权重值。这使得开发人员可以更精确地控制React的"reconciliation"过程,从而优化性能。

react之前的版本用expirationTime属性代表优先级,该优先级和IO不能很好的搭配工作(io的优先级高于cpu的优先级),现在有了更加细粒度的优先级表示方法LaneLane用二进制位表示优先级,二进制中的1表示位置,同一个二进制数可以有多个相同优先级的位,这就可以表示‘批’的概念,而且二进制方便计算。

类似于赛车车道,越靠近内圈的赛道越短,越靠近外圈的赛道越长,react通过31位的二进制来表示31条赛道,位数越小的赛道优先级越高。

那么,React是怎么实现渲染过程的可中断呢?

聊完了动态优先级,我们再深入了解一下,什么是可中断的渲染,React是怎么实现渲染过程的可中断呢?

React Fiber中,渲染过程是分"帧"进行的。每个帧都是一段时间,在这段时间内,React可以执行一些工作,然后将控制权返回给浏览器。这允许浏览器在渲染过程中处理用户输入或执行其他工作,从而避免阻塞整个 UI。

在每个帧中,React会执行一些"任务",这些任务可以是渲染组件、调用生命周期方法、执行数据请求等。React会根据每个任务的权重值(动态优先级)来决定执行顺序。

当React在执行任务时,如果发现某个任务的执行时间超过了当前帧的剩余时间,它就会暂停执行,并在下一帧继续执行。这就是React Fiber如何实现渲染过程的暂停和恢复的。

通过这种方式,React可以在渲染过程中保证浏览器的响应性,并避免阻塞整个 UI。这可以提高用户体验,特别是在处理大型或复杂的 UI 时。

那么,Fiber是怎么做到让出控制权的?

在构建和更新用户界面时,React Fiber 会分批处理任务,并在每批任务之间让出控制权,以便浏览器可以执行其他操作。这样,React Fiber 就可以在不阻塞浏览器的情况下执行大量任务,并提高 UI 渲染的性能。

React Fiber 使用让出控制权的技术来提高 UI 渲染的性能,但这并不意味着它会一直让出控制权。当浏览器可用的 CPU 资源充足时,React Fiber 可以继续执行任务而不让出控制权,以便尽快完成 UI 渲染。

在 React 中,让出控制权的技术通常是通过使用浏览器的 requestIdleCallback 函数实现的(react通过MessageChannel + requestAnimationFrame自己模拟实现了requestIdleCallback)。

这个函数允许在浏览器空闲时调用回调函数,并且可以指定最多可以使用多长时间的 CPU 资源。React Fiber 可以使用这个函数来在浏览器空闲时执行任务,从而让出控制权。

为什么React自己模拟实现了requestIdleCallback?

为什么不是setTimeout?

因为setTimeout的递归层级过深的话,延迟就不是1ms,而是4ms,这样会造成延迟时间过长

为什么不是requestAnimationFrame?

requestAnimationFrame是在微任务执行完之后,浏览器重排重绘之前执行,执行的时机是不准确的。如果raf之前JS的执行时间过长,依然会造成延迟。

与setTimeout相比,requestAnimationFrame最大的优势是由系统来决定回调函数的执行时机。(如果屏幕刷新率是60Hz,那么回调函数就每16.7ms被执行一次)

为什么不是requestIdleCallback?

requestIdleCallback的执行时机是在浏览器重排重绘之后,也就是浏览器的空闲时间执行。其实执行的时机依然是不准确的。

为什么是 MessageChannel?

首先,MessageChannel的执行时机比setTimeout靠前。其次,requestIdleCallback并不是所有浏览器都支持的。为了解决这个问题,React采用MessageChannel来模拟requestIdleCallback。

image.png

image.png

React使用它可以用来模拟requestIdleCallback的行为,例如在主线程执行 UI 更新,并在工作线程中执行其他计算密集型任务。

在这种情况下,React可以在主线程中执行 UI 更新,并在工作线程中执行非 UI 任务。这样就可以避免阻塞主线程,提高性能。

MessageChannel pr参考:Use setImmediate when available over MessageChannel

Fiber 为什么必须是链表,数组不行吗

Fiber 采用链表数据结构的原因是因为链表可以方便地在列表的中间插入和删除元素。这在构建和更新用户界面时非常有用,因为可能会有大量的元素需要插入或删除。

与数组相比,链表具有更好的插入和删除性能,因为在数组中执行这些操作通常需要移动大量元素,而在链表中只需要修改一些指针即可。

链表缺点:然而,链表的查找性能通常比数组差,因为需要遍历整个列表才能找到所需的元素。

尽管如此,Fiber 还是选择使用链表作为其数据结构,因为在构建和更新用户界面时插入和删除元素的需求要远远大于查找元素的需求。

那么,Fiber为什么能提高性能?

从上面的结论来看,

那么,Fiber的Reconcilation和DOM Diff有什么区别

"DOM diff"通常指的是在浏览器中执行的操作,而React的"reconciliation"则在应用程序的 JavaScript 代码中进行。

与 DOM diff 算法相比,React 的 Reconcilation 系统有一个显著的优势。DOM diff 算法是一种在两棵树之间找到最小补丁集的算法。它需要遍历整棵树来寻找变化,这是一个 O(n^3) 的复杂度的算法。相比之下,React 的 Reconcilation 算法的复杂度是 O(n) 级别的,因为它只遍历了有变化的节点。

React 调和为什么能使性能更好?

前面我们提到,React15是采用递归的方式创建虚拟DOM,递归过程是不能中断的。因此容易发生卡顿的现象。那么,React Reconcilation为什么能使性能更好呢?

在 react v16 之前,React 是直接递归渲染 vdom 的,setState 会触发重新渲染,对比渲染出的新旧 vdom,对差异部分进行 dom 操作。

在react v 16 之后,为了优化性能,会先把 vdom 转换成 fiber,也就是从树转换成链表,然后再渲染。整体渲染流程分成了两个阶段:

从 vdom 转成 fiber 的过程叫做 reconcile(调和)。这种机制使得 React 的性能更高,因为它减少了对 DOM 的操作。在大多数情况下,只需要修改 DOM 中的一小部分元素,而不是整个重新渲染。

Fiber Vs Stack Demo

从上面两个demo的对比来看,我们可以看到Fiber对动画的处理更丝滑。

那么,Fiber架构如何更好实现动画支持的呢?

React Fiber提供了一种新的方式来实现动画,称为"requestAnimationFrame"机制。这种机制可以在浏览器下一次重绘之前处理动画。

在React的传统架构中,动画是通过使用setInterval或setTimeout来实现的,这意味着动画的帧率可能不是很稳定,并且它可能会占用大量的CPU时间。

相反,Fiber架构中的requestAnimationFrame机制可以更好地控制动画的帧率,并且不会占用太多的CPU时间。这意味着动画可以更流畅,并且在大型应用程序中不会对性能造成太大的影响。

除了改进动画性能之外,Fiber架构还提供了一种新的方式来实现动画,称为"animation scheduling"。这种机制允许开发人员更精细地控制动画的时间进程,并且可以使动画更加流畅。

Fiber架构与React的传统架构有什么不同?

React Fiber是一个升级版的React的架构,它与React的传统架构有一些显著的区别:

尽管Fiber架构与传统架构有许多区别,但它们之间的差异对于开发人员来说并不是很明显。在大多数情况下,开发人员可以继续使用React的标准方式来开发应用程序,而无需了解底层实现细节。