React 理念
React 的理念 是:
我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。
可以看出「快速响应」是 React 所追求的,但是 CPU 瓶颈和 IO 瓶颈会制约快速响应。
CPU 瓶颈
在遇到大计算量的操作或是设备性能不足的时候,JS 就可能执行时间过长。而主流浏览器每 16.6ms 刷新一次,且 JS 脚本执行和浏览器布局绘制互斥。如果 JS 脚本的执行时间超过了 16.6ms,那么这次刷新就没有时间再进行布局绘制,导致了页面掉帧卡顿。
解决这个问题的方法是,每一帧只能在一个时间限度内执行 JS,如果时间不够用,就需要中断并等待下一帧再继续执行,而这一帧的剩余时间用来渲染 UI,即 需要实现时间切片。
IO 瓶颈
网络延迟时,需要等待较长的时间数据才返回,然后才能进行下一步操作,导致不能够快速响应。
解决这个问题的方法是,在当前的页面先停留一小段时间,在这一小段时间内请求数据,如果请求时间超过一个范围时再出现 loading 效果,减少用户对于请求过程的感知。
为什么要重构
为了解决上述的两个瓶颈,支持实现其解决方法,就需要实现 可中断的异步更新。而为了实现它,React 15 到 React 16 也重构了整个架构。下面就来康康为什么需要重构,以及新旧架构的整体区别。
React 15
React 15 架构分为两层:
- Reconciler(协调器)即 render 协调阶段
- Renderer(渲染器)即 commit 渲染阶段
组件更新时,Reconciler 会执行 diff 算法,递归处理虚拟 DOM,找出变化的部分并通知 Renderer。而 Renderer 在接到通知后,会把变化的组件渲染到页面上。Reconciler 和 Renderer 交替工作,整个过程都是同步的。
如果递归层级很深,Reconciler 工作超过了 16.6ms,那就会造成页面掉帧卡顿。而假设中途中断了更新,用户会看到的是更新不完全的 DOM,所以 React 15 架构并不能支持 可中断的异步更新,因此 React 进行了重构。
React 16
React 16 架构分为三层:
- Scheduler(调度器)即 schedule 调度阶段
- Reconciler(协调器)即 render 协调阶段
- Renderer(渲染器)即 commit 渲染阶段
在 React 16 架构中增加了 Scheduler,用于调度任务的优先级,高优先级的任务进入 Reconciler。在 Reconciler 中,从递归变成了可中断的循环过程,给变化的虚拟 DOM 打上代表增/删/更新的标记。而 Scheduler 和 Reconciler 都在内存中进行工作,在所有组件完成后,才会交给 Renderer,来执行对应的 DOM 操作。所以在前两个阶段完成前,不会更新页面上的 DOM,即使过程中反复中断,用户也不会看到更新不完全的 DOM。也就从 React 15 架构的同步更新,到重构后实现了 可中断的异步更新。
React 中的优先级
Scheduler 用于调度任务的优先级,那“优先级”又是怎么确定的呢?
用户交互的时候,会对交互的执行顺序有个心理预期。因此在 React 中人为地划分了等级,并且有一套从事件到调度的优先级机制,包括事件优先级、更新优先级、任务优先级、调度优先级。
在事件注册阶段,根据交互紧急程度确定了事件优先级,包括:
- 离散事件。如 click 等,特点是不连续触发,优先级为0
- 用户阻塞事件。如 drag、scroll、mouseover 等,特点是连续触发,会阻塞渲染,优先级为1
- 连续事件。如 canplay 等,特点是应该立即同步执行,不能被打断,优先级为2
事件执行时,会根据事件优先级计算它的更新优先级。
在即将调度的时候,根据更新优先级计算任务优先级。如果后一个更新的任务优先级高于前一个更新,那么会让 Scheduler 取消前一个的任务调度;如果前后两个更新的任务优先级相等,会将这两个更新收敛到一次任务中;如果后一个更新的任务优先级低于前一个更新,那么会在前一个更新完成后,再次用 Scheduler 对后者发起一次任务调度。以此来保证高优先级任务及时响应,收敛同等优先级的任务调度。
任务被调度后,就进入了 Scheduler,根据任务优先级计算调度优先级。在 Scheduler 中是用过期任务队列和未过期任务队列来管理内部的任务。在过期任务队列中,最早过期的排在最前面,会被最先处理。而过期时间,就是由调度优先级计算得出的。
综上,优先级就是由 事件优先级 → 更新优先级 → 任务优先级 → 调度优先级,这样层层递进计算出来的。
Fiber 和 React 的关系
React 16 架构中在 Reconciler 内部采用了 Fiber。
可中断的异步更新 践行了函数式编程中 代数效应 的思想,也就是将副作用从函数调用中分离。而浏览器原生的 Generator 就支持类似的实现。之所以没有采用 Generator 来实现 Reconciler,一方面是因为 Generator 是传染性的,使用了它的上下文其它函数也需要改变;另一方面是因为 Generator 执行的中间状态是上下文关联的,如果执行中途有高优的任务插队,那么已经计算完成的部分将不能复用,还需要重新计算。
所以 React 内部实现了一套状态更新机制,即 React Fiber,支持任务不同优先级,可以中断和恢复,恢复后也可以复用之前的中间状态。