fiber

246 阅读7分钟

一、问题

1.有react fiber,为什么不需要vue fiber
2.之前递归遍历虚构dom树被打断就得从头开始,为什么有了react fiber就能断点恢复呢;

二、react、vue的响应式原理

- 修改数据时,react需要调用setState方法,而vue直接修改变量就行。看起来只是两个框架的用法不同罢了,但响应式原理正在于此。从底层实现来看修改数据:在react中,组件的状态是不能被修改的,setState没有修改原来那块内存中的变量,而是去新开辟一块内存,而vue则是直接修改保存状态的那块原始内存。所以经常能看到react相关文章里经常会出现一个词'immutable',翻译过来就是不可变的。
- 数据改变了接下来要解决视图的更新:react中,调用setState方法后,会自顶向下重新渲染组件,自顶向下的含义是,该组件以及他的子组件全部需要渲染;而vue使用Object.defineProperty(vue@3迁移到了Proxy)对数据的设置(setter)和获取(getter)做了劫持,也就是说,vue能准确知道视图模板中那一块用到了这个数据,并且在这个数据修改时,告诉这个视图,你需要重新渲染了。
- 所以当一个数据改变,react的组件渲染是很消耗性能的--父组件的状态更新了,所有的子组件得跟着一起渲染,它不能像vue一样,精确到当前组件的粒度。
- 例子:分别用vuereact实现一个demo:父组件嵌套子组件,点击父组件的按钮会修改父组件的状态,点击子组件的按钮会修改子组件的状态。该例子中,react修改父组件状态,父子组件都会重新渲染。vue无论修改那个状态,组件都只会重新渲染最小颗粒。

三、不同响应式原理的影响

- 首先要强调的是,上文提到的"渲染render"更新都不是指浏览器真正渲染出视图。而是框架在JavaScript层面上,调用自身实现的render方法,生成一个普通的对象,这个对象保存了真实dom的属性,也就是常说的虚拟dom。本文会用组件渲染和页面渲染对两者做区分。
- 每次的视图更新流程是这样的:

     1.组件渲染生成一颗新的dom树
     2.新旧虚拟dom树对比,找出变动的部分
     3.为真正改变的部分创建真实dom,把他们挂载到文档,实现页面重渲染。

- 由于react和vue的响应式实现原理不同,数据更新时,第一步中react组件会渲染出一颗更大的虚拟dom树。

四、fiber是什么?

- 上面说了这么多都是为了方便讲清楚为什么需要,react fiber:在数据更新时,react生成了一颗更大的虚拟dom树,给第二步的diff带来了巨大的压力,我们想找到真正变化的部分,这需要花费更长的时间。js占据主线程去做比较,渲染线程便无法做其他的工作,用户的交互得不到响应,所以便出现了react filber。
- react fiber没法让比较的时间缩短,但它使diff的过程被分成一小段一小段的,因为它有了"保存工作进度"的能力。js会比较一部分虚拟dom,然后让渡主线程,给浏览器去做其他的工作,然后继续比较,依次往复,等到最后比较完成,一次性更新到视图上。

五、fiber是一种新的数据结构

- 上文提到了,react fiber使得diff阶段有了被保存工作进度的能力,这部分会讲清楚为什么。
- 我们要找到前后状态变化的部分,必须把所有节点遍历。
- 在老的架构中,节点以树的形式被组织起来:每个节点上有多个指针指向子节点,要找到两颗树的变化部分,最容易想到的办法就是深度优先遍历,规则如下:

  1.从根节点开始,依次遍历该节点的所有子节点;
  2.当一个节点的所有子节点遍历完成,才认为该节点遍历完成

  - 这其实是深度优先遍历的后序遍历。根据这个规则,在途中标出了节点的完后遍历的顺序。
  - 这种遍历有一个特点,必须一次性完成。假设遍历发生了中断,虽然可以保留当下进行中节点的索引,下次继续时,我们的确可以继续遍历该节点下面的所有子节点,但是没有办法找到其父节点--因为每个节点只有其子节点的指向。断点没有办法恢复,只能从头再来一遍。
- 在新的架构中,每个节点有三个指针:分别指向第一个子节点、下一个兄弟节点、父节点。这种数据结构就是fiber,它的遍历规则如下:
  1.从根节点开始,依次遍历该节点的子节点、兄弟节点,如果两者都遍历了,则回到它的父节点;
  2.当一个节点的所有子节点遍历完成,才认为该节点遍历完成

- 根据这个规则,同样在图中标出了节点遍历完成的顺序。跟树结构对比会发现,虽然数据结构不同,但是节点的遍历开始和完成顺序一模一样。不同的是,当遍历发生中断时,只要保留下当前节点的索引,断点是可以恢复的--因为每个节点都保持着对其父节点的索引。这就是react fiber的渲染可以被中断的原因。树和fiber虽然看起来很像,但本质上来说,一个是树,一个是链表。

六、fiber是纤程

- 这种数据结构之所以被叫做纤程,因为fiber的翻译是纤程,它被认为是协程的一种实现形式。协程是比线程更小的调度单位:它的开启、暂停可以被程序员所控制。具体来说,react fiber是通过requestIdCallback这个api去控制的组件渲染的"进度条"。requestIdCallback是一个属于宏任务的回调,就像setTimeout一样。不同的是,setTimeout的执行时机由我们传入的回调时间去控制,requestIdCallback是受屏幕的刷新率去控制。本文不对这部分做深入的探究,只需要知道它每隔16ms会被调用一次,它的回调函数可以获取本次可以执行的时间,每一个16ms除了requestIdCallback的回调之外,还有其他工作,所以能使用的时间是不确定的,但只要时间到了,就会停下节点的遍历。
- requestIdCallback的回调函数可以通过传入参数deadLine.timeRemaining()检查当下还有多少时间供自己使用,但由于兼容性不好,加上该回调函数被调用的频率太低,react实际使用的是一个polyfill(自己实现的api),而不是requestIdCallback。
- 总结:react fiber是react 16提出的一种更新机制,使用链表取代了树,将虚拟dom连接,使得组件更新的流程可以被中断恢复;它把组件渲染的工作分片,到时会主动让出渲染主线程。

七、react fiber带来的变化

八、react不如vue吗

- 我们现在知道了react fiber是在弥补更新时"无脑"刷新,不够精确带来的缺陷,这是不是能说明react性能更差呢?并不是,因为vue实现精确更新也是有代价的,一方面是需要给每一个组件配置一个"监视器",管理着视图的依赖收集和数据更新时的发布通知,这对性能同样是有消耗的;另一方面vue能实现依赖收集得益于它的模板语法,实现静态编译,这是使用更灵活的JSX语法的react做不到的。
- 在react fiber出现之前,react也提供了PureComponent、shouleComponentUpdate、useMemo、useCallback等方法给我们,来声明那些是不需要连带更新子组件。

九、结语

- react因为先天的不足--无法精确更新,所以需要react fiber把组件渲染工作切片;而vue基于数据劫持,更新粒度很小,没有这个压力;
- react fiber这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;