React进阶笔记【8_Fiber入门】

2022/4/5 ReactJS

React 的 Stack Reconciler 在体验方面显得有些疲态,在 React 16.x 版本将其最为核心的 Diff 算法全部重写,使其以 “Fiber Reconciler” 的全新面貌示人。

# 前言

多线程的浏览器除了要处理 JavaScript 线程以外,还需要处理各种各样的任务线程,其中也包括负责处理 DOM 的 UI 渲染线程,而 JavaScript 线程是可以操作 DOM 的。如果渲染线程和 JavaScript 线程同时工作,那么渲染结果必然是难以预测的。

JavaScript 线程和渲染线程必须是互斥的:当其中一个线程执行时,另一个线程只能挂起等待。

类似特征的还有事件线程,浏览器的 EventLoop 机制决定了事件任务是由一个异步队列维持的。当事件被触发时,对应的任务不会立刻被执行,而是由事件线程把它添加到任务队列的末尾,等待 JavaScript 的同步代码执行完毕后,在空闲的时间里执行出队。如果 JavaScript 线程长时间占用主线程,那么渲染层面的更新就不得不长时间地等待,而界面长时间不更新,带给用户的体验就是卡顿。而此时事件线程也在等待 JavaScript,这就导致触发的事件也将是难以响应的。

而 Stack Reconsiler 是一个同步的递归过程,它带来的问题就是 JavaScript 对主线程的超时占用问题。

“Stack Reconciler” 是 React 15 及更早期的调和器,主要负责组件的挂载、卸载、更新等过程,而更新也就是 Diff。

栈调和机制下的 Diff 算法,其实就是树的深度优先遍历的过程,其总是和递归有关。调和器会重复 “父组件调用子组件” 的过程,直到最深的一层节点更新完毕,才慢慢向上返回。

# 什么是 Fiber

  • 从架构角度来看,Fiber 是对 React 核心算法的重写。
  • 从编码角度看,Fiber 是 React 内部所定义的一种数据结构,是 Fiber 树结构的节点单位,也就是 React 16 新架构下的虚拟 DOM。
  • 从工作流角度来看,Fiber 节点保存了组件需要更新的状态和副作用,一个 Fiber 对应一个工作单元。

# Fiber 是如何解决问题的

Fiber 架构的应用目的是实现 “增量渲染”,也就是说把一个渲染任务分解为多个渲染任务,而后将其分散到多个帧里面。

实现 “增量渲染” 的目的,是为了实现任务的可中断、可恢复,并给不同的任务赋予不同的优先级,最终达成更加顺滑的用户体验。

在 React 16 之后,更新处理的工作流变成了:

  1. 每个更新任务都会被赋予一个优先级
  2. 当更新任务 A 抵达调度器时,高优先级的任务会被优先放进 Reconciler 层
  3. 此时若有新的更新任务 B 出现,会判断 B 的优先级
  4. 若 B 的优先级高于 A,那么当前处于 Reconciler 层的 A 就会被中断
  5. 调度器会将 B 推进 Reconciler 层
  6. 当 B 执行结束,新一轮的调度任务开始执行
  7. 之前被中断的 A,就会被重新推入 Reconciler 层,继续执行,即可恢复

# Fiber 对生命周期的影响

在 render 阶段,React 主要是在内存中做计算,明确 DOM 树的更新点,而 commit 阶段负责把 render 生成的更新真正的执行掉。新老架构对 React 生命周期的影响主要在 render 阶段,这个影响是通过增加 Scheduler 层和改写 Reconciler 层来实现的。

render 阶段,一个更新任务会被拆分为一个个的工作单元,这些工作单元有着不同的优先级,React 可以根据优先级的高低去实现工作单元的打断和恢复。由于 render 阶段对用户是不可见的,所以打断再重启对于用户是无感知的。但是工作单元(也就是任务)的重启将伴随着对部分生命周期的重复执行:

  1. componentWillMount
  2. componentWillUpdate
  3. shouldComponentUpdate
  4. componentWillReceiveProps

其中 shouldComponentUpdate 的结果是 true 或者 false,以代表是否有必要更新,该函数一般不会进行副作用,而其他三个生命周期,经常容易被滥用。