一. React 15 中的虚拟 DOM 和 Diff 存在的缺陷
<1> 浏览器的运行机制
浏览器刷新频率: 60HZ 也就是每秒刷新 60 次,大概 16.6ms 浏览器刷新一次(记住这个跟性能密切相关的数字 16.6)
由于
GUI 渲染线程和JS 线程是互斥的,所以 JS 代码执行和浏览器布局、绘制不能同时执行。在这 16.6ms 的时间里,浏览器既需要完成 JS 的执行,也需要完成样式的重排和重绘,如果 JS 执行的时间过长,超出了 16.6ms,这次刷新就没有时间执行样式布局和样式绘制了,于是在页面上就会表现为卡顿。【且在 React 15 中,JS的diff过程是不可中断的】!!!
<2> React 15 的架构
React 15 的架构,主要包含以下两块内容:
-
Reconciler:(协调器)负责调用 render 生成虚拟 DDOM,进行 Diff,找出变化后的虚拟 DOM -
Renderer:(渲染器)负责接收 Reconciler 通知,将变化的组件渲染在当前宿主环境,比如浏览器(react-dom),不同的宿主环境会有不同的 Renderer
<3> 存在缺陷
当 React 状态更新时,会递归同步更新 DOM 树 。如果节点非常多,即使只有一次 state 变更,【React 也需要进行复杂的递归更新,更新一旦开始,中途就无法中断,直到遍历完整棵树,才能释放 JS 主线程。】 如果 React 组件内容过多,就会导致在 16.6 ms 内无法完成全部的状态更新而导致没有足够的时间去执行浏览器的渲染,于是在页面上就表现为卡顿。
二. Fiber 架构腾空出世
调度 Scheduling
可以理解为:确定什么时候应该执行某段代码。【调度核心理念便是:我们可以随心所欲的控制我们的代码。】React 目前没有很大程度的利用调度,所以才会引入Fiber。
所以具体来说,我们需要 Fiber 实现:
- 暂停工作,稍后再回来。
- 为不同类型的工作分配优先级。
- 重用之前完成的工作。
- 如果不再需要,则终止工作。
所以,Fiber可以理解如下:
我们首先需要一种将工作分解为单元的方法。从某种意义上说,这就是Fiber。一个 Fiber 代表一个工作单元。
<1> Fiber 架构
Scheduler(调度器):调度任务的优先级,高优任务优先进入 Reconciler- Reconciler(协调器):负责找出变化的组件(使用
Fiber重构) - Renderer(渲染器):负责将变化的组件渲染到页面上
Fiber工作原理:
-
每个 Fiber 对应一个 React 元素,每个 Fiber 都有 3 个属性:child(第一个子节点)、sibling(兄弟节点)、return(父级节点)。
-
这样,将来 React 在进行 diff 时,就可以按照 Fiber 之间的顺序,一个个的执行,比如,可以把每个 Fiber 节点看成是一个小任务。
-
每个小任务完成后就会看下是否还有剩余时间,如果有就继续处理下一个 Fiber;如果没有剩余时间,就将当前处理的 Fiber 引用存储起来,然后,将控制权交还给浏览器,
-
由浏览器完成渲染。然后,下一帧,React 恢复上一次的工作,接着处理下一个 Fiber 工作。所以,这个阶段的工作是可中断、可恢复执行的。
<2> 双缓存 Fiber 树
React 做更新处理时,会同时存在两颗 fiber tree。一颗是已经存在的 old fiber tree,对应当前屏幕显示的内容,通过根节点 fiberRootNode 的 currrent 指针可以访问,称为 current fiber tree;另外一颗是更新过程中构建的 new fiber tree,称为 workInProgress fiber tree。
这两棵树之间通过 alternate 属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
双缓存下的diff比较:
diff 比较,就是在构建 workInProgress fiber tree 的过程中,判断 current fiber tree 中的 fiber node 是否可以被 workInProgress fiber tree 复用。
当更新完成以后,fiberRootNode 的 current 指针会指向 workInProgress fiber tree,作为下一次更新的 current fiber tree。
<3> Concurrent Mode - 并发模式
Concurrent 模式是一组 React 的新功能,可帮助应用保持响应,并根据用户的设备性能和网速进行适当的调整
总的来说,有以下两个策略:
(1) 大任务拆分小任务
-
React 的解决思路,就是在浏览器每一帧的时间中预留一些时间给 JS 线程,React 利用这部分时间更新组件。
-
当预留的时间不够用时,React 将线程控制权交还给浏览器让他有时间渲染 UI,React 则等待下一帧再继续被中断的工作。
-
将大任务分拆到每一帧中,每一帧执行一小部分任务的操作,就是我们常说的:时间切片
(2) 任务划分优先级
- 让组件的渲染 “可中断” 并且具有 “优先级”。