在 react 进入你们视野之初,Virtual DOM(VDOM)的概念让人眼前一亮,在操做真正的 DOM 以前,先经过 VDOM 先后对比得出须要更新的部分,再去操做真实的 DOM,减小了浏览器屡次操做 DOM 的成本。这一过程,官方起名 reconciliation,可翻译为协调算法
。可是 react 发展到今日,随着前端应用的量级愈来愈大,reconciliation 已经日显疲惫,React Fiber 应运而出。React Fiber 是对 React 核心算法的重写,由 React 团队历时两年多完成。html
当时被你们拍手叫好的 VDOM,为何今日会略显疲态,这还要从它的工做原理提及。在 react 发布之初,设想将来的 UI 渲染会是异步的,从 setState()
的设计和 react 内部的事务机制能够看出这点。在 react@16 之前的版本,reconciler(现被称为 stack reconciler )采用自顶向下递归,从根组件或 setState()
后的组件开始,更新整个子树。若是组件树不大不会有问题,可是当组件树愈来愈大,递归遍历的成本就越高,持续占用主线程,这样主线程上的布局、动画等周期性任务以及交互响应就没法当即获得处理,形成顿卡的视觉效果。前端
理论上人眼最高能识别的帧数不超过 30 帧,电影的帧数大多固定在 24,浏览器最优的帧率是 60,即16.5ms 左右渲染一次。 浏览器正常的工做流程应该是这样的,运算 -> 渲染 -> 运算 -> 渲染 -> 运算 -> 渲染 …node
可是当 JS 执行时间过长,就变成了这个样子,FPS(每秒显示帧数)降低形成视觉上的顿卡。react
那么这个问题如何解决,这就是 fiber reconciler 要作的事了。简而言之能够看下图,将要执行的 JS 作拆分,保证不会阻塞主线程(Main thread)便可。git
将同步任务拆分你们都能理解,但在拆分以前咱们面临如下几个问题:github
# React@15
DOM 真实DOM节点
-------
Instances React 维护的 VDOM tree node
-------
Elements 描述 UI 长什么样子(type, props)
复制代码
在 react@15 中,更新主要分为两个步骤完成: 1. diff diff 的实际工做是对比 prevInstance 和 nextInstance 的状态,找出差别及其对应的 VDOM change。diff 本质上是一些计算(遍历、比较),是可拆分的(算一半待会儿接着算)。 2. patch 将 diff 算法计算出来的差别队列更新到真实的 DOM 节点上。React 并非计算出一个差别就执行一次 patch,而是计算出所有的差别并放入差别队列后,再一次性的去执行 patch 方法完成真实的DOM更新。算法
最后的 patch 阶段更新,是一连串的 DOM 操做,虽然能够根据 diff 后获得的 change list 作拆分,可是意义不大,不只会致使内部维护的 DOM 状态和实际的不一致,也会影响体验,因此应该作的是对 diff 阶段进行拆分。从下图是 ReactDOM 渲染 10000 个子组件的过程。能够看到,在 diff 执行阶段主线程一直被占用,没法进行其余任何操做 I/O 操做,直到运行完成。浏览器
由此引出了 React Fiber 的解决方案,以一个 fiber 为单位来进行拆分,fiber tree 是根据 VDOM tree 构造出来的,树形结构彻底一致,只是包含的信息不一样。如下是 fiber tree 节点的部分结构:bash
{
alternate: Fiber|null, // 在fiber更新时克隆出的镜像fiber,对fiber的修改会标记在这个fiber上
nextEffect: Fiber | null, // 单链表结构,方便遍历 Fiber Tree 上有反作用的节点
pendingWorkPriority: PriorityLevel, // 标记子树上待更新任务的优先级
stateNode: any, // 管理 instance 自身的特性
return: Fiber|null, // 指向 Fiber Tree 中的父节点
child: Fiber|null, // 指向第一个子节点
sibling: Fiber|null, // 指向兄弟节点
}
复制代码
Fiber 依次经过 return、child 及 sibling 的顺序对 ReactElement 作处理,将以前简单的树结构,变成了基于单链表的树结构,维护了更多的节点关系。架构
Stack 在执行时是以一个 tree 为单位处理;Fiber 则是以一个 fiber 的单位执行。Stack 只能同步的执行;Fiber 则能够针对该 Fiber 作调度处理。也就是说,假设如今有个 Fiber 其单链表(Linked List)结构为 A → B → C,当 A 执行到 B 被中断的话,能够以后再次执行 B → C,这对 Stack 的同步处理结构来讲是很难作到的。
在 React Fiber 执行的过程当中,主要分为两个阶段(phase):
第一个阶段主要工做是自顶向下构建一颗完整的 Fiber Tree, 在 rerender 的过程当中,根据以前生成的树,构建名为 workInProgress 的 Fiber Tree 用于更新操做。
假设我有上图所示的 DOM 结构须要渲染,第一次 render 的时候会生成下图所示的 Fiber Tree:
由于我须要对 Item 里面的数值作平方运算,因而我点击了 Button,react 根据以前生成的 Fiber Tree 开始构建workInProgress Tree。在构建的过程当中,以一个 fiber 节点为单位自顶向下对比,若是发现根节点没有发生改变,根据其 child 指针,把 List 节点复制到 workinprogress Tree 中。 每处理完一个 fiber 节点,react 都会检查当前时间片是否够用,若是发现当前时间片不够用了,就是会标记下一个要处理的任务优先级,根据优先级来决定下一个时间片要处理什么任务。
requestIdleCallback 会让一个低优先级的任务在空闲期被调用,而 requestAnimationFrame 会让一个高优先级的任务在下一个栈帧被调用,从而保证了主线程按照优先级执行 fiber 单元。 优先级顺序为:文本框输入 > 本次调度结束需完成的任务 > 动画过渡 > 交互反馈 > 数据更新 > 不会显示但以防未来会显示的任务。
module.exports = {
// heigh level
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // For controlled text inputs.
TaskPriority: 2, // Completes at the end of the current tick.
AnimationPriority: 3, // Needs to complete before the next frame.
// low level
HighPriority: 4, // Interaction that needs to complete pretty soon to feel responsive.
LowPriority: 5, // Data fetching, or result from updating stores.
OffscreenPriority: 6, // Won't be visible but do the work in case it becomes visible. }; 复制代码
在平方运算这一过程当中,react 经过依次对比 fiber 节点发现 List,Item2,Item3 发生了变化,就会在对应生成的 workInProgress Tree 中打一个 Tag,而且推送到 effect list 中。
当 reconciliation 结束后,根节点的 effect list 里记录了包括 DOM change 在内的全部 side effect,在第二阶段(commit)执行更新操做,这样一个流程就算结束了。
在这个示例中,详细的比对流程并无细讲,推荐观看 Lin Clark 去年 react conf 中的演讲,很是浅显易懂,本文中示例也来自这个演讲。
will
生命周期可能被屡次调用而影响性能。react 团队给了咱们很长一段时间来处理这个问题,官方也提供了不少参考案例,能够平滑过渡到下个版本。react@16 与其说是一个分水岭,不如说是一个过渡,作的不少工做都是在给用户打预防针,告诉你接下来该怎么作,react@17才会是掀起风浪的那一个。reconciliation 的重写给 react 的将来带来太多的可能,包括最近社区讨论的如火如荼的 Hooks,其实也是 Fiber 带来一种可能性。在后续的版本中,我的觉得写法上会有不小的改变,主要是为了更加优秀的性能服务;还有就是将一些社区产生的方案作优化,让写法更加人性化(HOC 中的 refs 以及 context 传递),以及对常见的问题给出官方的解决方案(异步数据处理)等等。除了优势,固然也会带来些问题。随着版本的迭代,react 中的概念愈来愈多,新手学习的曲线只怕是会愈来愈陡峭。
在处理大型应用时,react 的表现不尽人意。主要缘由在于计算耗时太长,致使主线程一直被占用,没法处理其余任务。react 团队为了解决这个问题,提出了 Fiber reconciliation 的方案来代替以前的 Stack reconciliation。Fiber 相较于 Stack,采用了异步的方式将以前同步执行的计算过程作拆分,使得主线程不会一直处于被占用的状态,能够有时间去处理其余任务,好比 I/O 操做,交互反馈等。