协调器(reconciler)和渲染器(renderer)
协调和渲染被设计为两个独立的阶段。
协调器
协调器负责计算React元素树的哪些部分已更改,协调算法也通常被称为diff算法。
在React 15及更早的版本,一直是采用的Stack reconciler,协调器基于堆栈实现。而Fiber架构则重新实现了协调器,从React 16开始,Fiber reconciler变为了默认的协调器。Fiber reconciler的主要目的是:
- 能够把可中断的任务切片处理。
- 能够调整优先级,重置并复用任务。
- 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
- 能够在 render() 中返回多个元素。
- 更好地支持错误边界。
渲染器
React最初只适用于web浏览器,但之后也设计出了React Native用于支持原生平台。
渲染器其实就是根据不同的底层平台,调用对应的api,将协调器计算出的变更信息更新到界面上。
为何卡顿
React组件的状态产生变化时,会先后经历 reconciliation (协调)和 rendering (渲染)两个独立的过程。 (但也有说法将两个阶段命名为渲染和提交,其实这是源代码中的命名,这里的渲染阶段就是对应上面的协调阶段)
但现实往往很残酷。
随着页面复杂度的上升,上述过程会占用主线程更多的时间,浏览器资源被React霸占,页面失去响应、动画掉帧,用户可以感知到卡顿。React元素树的庞大会使得在协调阶段花费更多资源来计算更新;如果需要更新的节点很多,则会花费更多的资源在渲染阶段。
在处理UI时,问题在于如果一次执行太多工作,可能会导致动画掉帧…
给浏览器一点喘息的机会,他会对代码进行编译优化(JIT)及进行热代码优化,或者对reflow进行修正。
卡顿的原因找到了:就是一次性做了太多的工作,阻塞了主线程。
requestIdleCallback
目前某些浏览器(Chrome)的新版本已经实现了有助于解决这个问题的api:requestIdleCallback,该函数可用于在浏览器事件循环空闲期间将要调用的函数排队。具体api细节点这里
requestIdleCallback实际上有点过于严格,并且执行频率不足以实现流畅的UI呈现,因此React团队不得不实现自己的版本。
为了使用这种api,React需要把工作分解为增量单位,并可以随意中断和恢复工作。
调用堆栈的改进
如果仅依靠[内置]调用堆栈,它将一直工作到堆栈为空为止……如果我们能够随意中断调用堆栈并手动操作堆栈帧,那不是很好吗?这就是React Fiber的目的。Fiber是堆栈的重新实现,专门用于React组件。 您可以将单个Fiber视为虚拟堆栈框架。
React协调程序先前实现了使用依赖于内置堆栈的同步递归模型来遍历React元素树。而现在想要实现中断任务执行,并控制任务重新开始,很明显堆栈结构是不适合的。因为函数调用栈是不能随意被中断的,并且很难被恢复,执行上下文在函数中断后需要被回收,如果要恢复则需要从头开始执行调用栈。因此React需要寻找更合适的数据结构来替换堆栈,放弃递归遍历这种方式。
假设我们具有以下组件树:
递归遍历的顺序为:
a1,b1,b2,c1,d1,d2,b3,c2
那么React如何实现算法而无需递归遍历树呢?它使用单链表树遍历算法。这样可以暂停遍历并阻止堆栈增长。
要实现该算法,我们需要一个包含3个字段的数据结构:
- 子级-引用第一个孩子
- 同级-引用第一个同级
- 返回-引用父级
下图演示了单链表的层次结构以及它们之间的连接类型:
如果使用单链表的遍历算法,调用堆栈将不会增长,如下图所示:
通过对调用堆栈的改进,给中断工作提供了可行性。
Fiber节点、节点树
您可以将Fiber视为要执行的某些工作,或者说一个工作单元的数据结构。Fiber的体系结构还提供了一种方便的方式来跟踪,调度,暂停和中止工作。
每个React元素都对应一个Fiber节点。首次将React元素转换为Fiber节点时,React依据元素的类型和数据,调用createFiberFromTypeAndProps函数创建Fiber节点,这些节点最终组成一个Fiber节点树。而在后续的更新中,React会重用已存在的Fiber节点,并根据不同的元素类型做不同的更新:对原生节点执行DOM操作,对React节点调用生命周期函数和render函数。
React会同时维护两棵Fiber节点树,current树和workInProgress树。前者对应已更新到屏幕上的状态,后者代表即将要刷新到屏幕的状态。所有的工作都是在workInProgress树的Fiber节点上进行的,当处理完更新并完成所有工作后,一次性地更新DOM(不会给用户显示半成状态),这棵workInProgress树便成为current树。这两棵树上的同一位置的节点,都有一个alternate字段保持互相引用。
workInProgress树就像是页面的草稿,如果在协调过程中发生了错误,React完全可以丢弃掉这棵树,并且由Error Boundary来决定具体渲染什么内容。
Effects list
You’ve likely performed data fetching, subscriptions, or manually changing the DOM from React components before. We call these operations “side effects” (or “effects” for short) because they can affect other components and can’t be done during rendering.
Fiber节点中有个字段叫effectTag,用来定义每个实例在更新完成后需要做的工作,具体有哪些类型可以查看这里。对于DOM节点来说是增删改,对于class组件可能需要额外更新refs、调用生命周期函数。
React会把具有Effect的Fiber节点串联起来,形成一个线性列表,以实现快速迭代,因为没有必要去花时间在没有副作用的节点上。每个节点都具有nextEffect属性,用来将列表连接起来,而不是像current树/workInProgress树一样使用child属性。
列表的串联是按照子节点->父节点的顺序。React使用firstEffect标记出列表的开头,整个列表如下所示:
再谈协调和渲染
生命周期为何是UNSAFE的?
React在两个工作阶段,协调和渲染,都会调用对应的生命周期函数。
在第一阶段,即协调阶段,工作是可以异步执行的,这里的异步指的是React可以根据可用时间,中断正在处理的工作并存储数据,让步于某个更高优先级的任务,过后从中断的地方继续;或者中断后放弃所做的工作,从头开始。协调阶段调用的生命周期函数包括:
- [UNSAFE_] componentWillMount(已弃用)
- [UNSAFE_] componentWillReceiveProps(已弃用)
- getDerivedStateFromProps
- shouldComponentUpdate
- [UNSAFE_] componentWillUpdate(不建议使用)
- render
而第二阶段也就是渲染阶段,执行工作是同步的,不可被中断,具体原因就是React不会将“半成品”展示到界面上。这个阶段的生命周期包括:
- getSnapshotBeforeUpdate
- componentDidMount
- componentDidUpdate
- componentWillUnmount
正因为第一阶段是可以被中断/重新执行的,那么这个阶段的生命周期函数就可以被执行超过一次,为了避免程序员在这些函数中执行副作用的代码,包括调用api、修改DOM等操作,React便将这些函数设置为UNSAFE,或者干脆直接弃用。
work loop(工作循环)
所有Fiber节点都在工作循环中处理。
协调阶段
调用renderRoot方法,从顶端节点开始遍历。在遍历过程中,React会跳过已经处理的Fiber节点,直到找到下一个未处理的节点。它使用nextUnitOfWork来保持对下一个未处理的Fiber节点的引用。对于父子节点都需要处理的情况,它也是按照子节点->父节点的顺序来处理。
工作都是在workInProgress树上进行的,协调算法将workInProgress树遍历过后,会在这棵树上标记好具有副作用的节点。另外还有一个effect list,有所有具有副作用的节点组成。workInProgress树和effect list是协调阶段的产物。
渲染阶段
此阶段调用了commitRoot方法,用来将更新渲染到屏幕上,并调用一些生命周期方法,具体顺序如下:
- 在标记
Snapshot的节点上调用getSnapshotBeforeUpdate - 在标记
Deletion的节点上调用componentWillUnmount - 执行DOM增删改操作
- 将workInProgress树设置为current树
- 在标记
Placement的节点上调用componentDidMount - 在标记
Update的节点上调用componentDidUpdate
总结
- React Fiber的目的是为了让界面看起来响应更快,利用时间分片实现增量渲染。
- React重新实现了协调器,使用单链表树遍历算法来遍历React元素树,并且把React元素转化为Fiber节点,保存工作内容并实现调度工作。
(晓黑板前端团队)
参考文章
Inside Fiber: in-depth overview of the new reconciliation algorithm in React
The how and why on React’s usage of linked list in Fiber to walk the component’s tree
这可能是最通俗的 React Fiber(时间分片) 打开方式
司徒正美:React Fiber架构
React Fiber Architecture