随着 React 16 的发布,Hooks 的正式上线,很多小伙伴都很兴奋,都想要尝试这一新的特性,升级 React 的意愿越来越强烈了。
我们都知道 React 是一个优秀的前端框架,很多的大型应用都在使用,而作为使用 React 为工具的开发者也应该了解下 React Fiber,Fiber 到底是什么?它能给我们带来什么?以及 React 团队为什么要去重写 Fiber 架构?
什么是 React Fiber
?
React Fiber 是 React 16 中新的协调引擎,是对核心算法的一次重新实现。
既然是新的,那 React 团队为什么要重写新的 Fiber 架构呢?
在 React 16 以前,当元素较多,需要频繁刷新的时候页面会出现卡顿,究其原因是因为更新过程是同步的,大量的同步计算任务阻塞了浏览器的渲染。
当页面加载或者更新时,React 会去计算和比对 Virtual DOM,最后绘制页面,整个过程是同步进行的。当 JavaScript 在浏览器的主线程上长期运行,就会阻塞了样式计算、布局和绘制,导致页面无法得到及时的更新和响应。此时,无论用户如何点击鼠标或者敲击键盘都不会得到响应,当 React 更新完成后刚刚点击或敲击的事件才会得到响应。
由于 JavaScript 是单线程的特点,所以一个线程执行完成后才会执行下一个线程,当上一个线程任务耗时太长,程序就会对其他输入不作出响应。
React 的更新过程会先计算,一旦任务开始进行,就无法中断, js 将一直占用主线程, 直到整棵 Virtual DOM 树计算完成之后,才能把执行权交给渲染引擎,而 React Fiber 就是要改变现状。
它能给我们带来什么?
增量渲染
为不同任务分配优先极
更新时能暂停、终止、复用渲染任务
并发方面新的能力
接下来,让我们具体来了解下 Fiber。
Fiber
Fiber 把耗时长的任务拆分成很多的小片,每个小片的运行时间很短,每次只执行一个小片,执行完后看是否还有剩余时间,如果有就继续执行下个小片,如果没有就挂起当前任务,将控制权交给 React 负责任务协调的模块,看有没有其他紧急任务要做,如果没有就继续更新当前任务,如果有紧急任务就去做紧急任务,等主线程不忙的时候在继续执行当前任务。
分片之后,每执行一段时间,都会将控制权交给主线程。
这样唯一的线程不会被独占,其他任务依然有运行的机会。
这种策略叫做 Cooperative Scheduling(合作式调度),操作系统常用任务调度策略之一。
总而言之,我们了解到,Fiber 是一个最小工作单元,也是堆栈的重新实现,可以理解为是一个虚拟的堆栈帧。它将可中断的任务拆分成多个任务,通过优先级来自由调度子任务,分段更新,从而将之前的同步渲染改为异步渲染。
Fiber 结构
维护每一个分片的数据结构,就是 Fiber,它可以用 JS 对象来表示,其中包含有关组件,以及输入和输出的信息:
const fiber = {
// 跟当前 Fiber 相关本地状态(比如浏览器环境就是 DOM 节点)
stateNode: any, // 节点实例
// 指向他在 Fiber 节点树中的 `parent`,用来在处理完这个节点之后向上返回
return: Fiber | null, // 父节点
// 单链表树结构
child: Fiber | null, // 指向自己的第一个子节点
sibling: Fiber | null, // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点
index: number,
// 组件相关
tag: WorkTag, // 标记不同的组件类型
key: null | string, // ReactElement 里面的 key,与 type 一起,主要用来在reconciliation 期间确定 Fiber 是否可重用。
elementType: any, // ReactElement.type,也就是我们调用 `createElement` 的第一个参数
type: any, // 对于复合组件,type 是函数或者是类组件(`function`或者`class`),对于标准组件(div或者span),type 是 string
// ref属性
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
// 更新相关
// 当传入的 pendingProps 和 memoizedProps 相同的时候,表示 fiber 可以重新使用之前的 fiber,以避免重复的工作。
pendingProps: any, // 新的变动带来的新的props
memoizedProps: any, // 上一次渲染完成之后的props
updateQueue: UpdateQueue<any> | null, // 该Fiber对应的组件产生的Update会存放在这个队列里面
memoizedState: any, // 上一次渲染的时候的state
firstContextDependency: ContextDependency<mixed> | null, // 一个列表,存放这个Fiber依赖的context
pendingWorkPriority: number, // 待处理的工作优先级,除 NoWork 为0外,数字越大表示优先级越低。
// Scheduler 相关
expirationTime: ExpirationTime, // 代表任务在未来的哪个时间点应该被完成,不包括他的子树产生的任务
// 快速确定子树中是否有不在等待的变化
childExpirationTime: ExpirationTime,
// 用来描述当前Fiber和他子树的`Bitfield`
// 共存的模式表示这个子树是否默认是异步渲染的
// Fiber被创建的时候他会继承父Fiber
// 其他的标识也可以在创建的时候被设置
// 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前
mode: TypeOfMode,
// 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
// 我们称他为`current <==> workInProgress`
// 在渲染完成之后他们会交换位置
alternate: Fiber | null,
// Effect 相关的
effectTag: SideEffectTag, // 用来记录Side Effect
nextEffect: Fiber | null, // 单链表用来快速查找下一个side effect
firstEffect: Fiber | null, // 子树中第一个side effect
lastEffect: Fiber | null, // 子树中最后一个side effect
}
任务的优先级
上面讲到了任务的执行是根据优先级来调度的,那我们现在具体了解一下优先级。
synchronous 同步执行,首屏使用
task 在 next tick 之前执行
animation 下一帧之前执行,通过requestAnimationFrame来调度,这样在下一帧就能立即开始动画过程
high 在不久的将来立即执行
low 稍微延迟(100-200ms)执行也没关系
offscreen 当前隐藏的、屏幕外的(看不见的)元素,在下一次 render 时或 scroll 时才执行
Fiber reconciler
Fiber Reconciler 决定了当任务调度完成之后,如何去执行每个任务,如何去更新每一个节点的过程。
React Fiber 更新过程分为两个阶段
第一阶段 Reconciliation Phase,生成 Fiber 树,得出需要更新的节点信息。是一个渐进的过程,可以被打断。可能会调用 componentWillMount,componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate 生命周期函数。
第二阶段 Commit Phase,将需要更新的节点批量更新,这个过程不能被打断。可能会调用 componentDidMount,componentDidUpdate,componentWillUnmount 生命周期函数。
Fiber tree 与 WorkInProgress Tree
// 单链表树结构
{
return: Fiber | null, // 指向父节点
child: Fiber | null, // 指向自己的第一个子节点
sibling: Fiber | null, // 指向自己的兄弟结构,兄弟节点的 return 指向同一个父节点
}
首次渲染之后 React 会得到一个 Fiber 树,也就是 Current tree(当前树)。当处理更新的时候,React 会构建 WorkInProgress Tree(工作过程树),当构造完成后会将 current 指针指向 WorkInProgress Tree,WorkInProgress Tree 成了新的 Fiber tree。
这被称做双缓冲。以 Fiber tree 为主,WorkInProgress Tree 为辅。
双缓冲技术可以复用内部对象(fiber),节省内存分配、GC的时间开销。
总结
本篇文章篇幅有限,大致讲了 Fiber 的相关知识和 React 的工作流程,对于细节,比如:如何调度任务,如何 diff 等,感兴趣的同学可以自行结合源码研究分析。