回顾
什么是协调
- 协调 reconciliation - React 比对树之间差异的算法,用来决定哪些部分需要更新。
- 更新 update - 通过数据的更新来重新渲染,通常由
setState
触发。
React API 的核心概念是将更新视为整个应用的重新渲染。这让开发者仅需维护好状态的变化,而不用考虑如何从一个状态转变到另一个状态。
当状态更新时,如果重新渲染整个应用,通常代价是非常昂贵的。而 React 有优化手段,仅去执行部分必要的渲染工作。这些优化的大部分都来自于 协调 reconciliation 的过程之中。
协调也常被理解为「虚拟 DOM」。具体一点的解释如下:当你在渲染一个 React 应用时,背后会建立一棵节点树并存在内存中。之后这棵树会同步给渲染环境 - 比如对于一个浏览器应用,它会被转为一组 DOM 操作。当应用更新时(通常通过 setState
),会生成一颗新树,这颗新树会与先前的树对比,来计算出需要执行哪些 DOM 操作来更新已渲染的应用。
尽管 Fiber 是对 reconciler 的彻底重写,但是 算法 大致是一致的。其中有这俩关键点:
- 对于不同的组件类型,React 会直接对整个旧树进行替换。
- 通过
key
来比对列表元素。key
应当是「稳定的、可预测的、唯一的」。
协调 vs 渲染
DOM 只是 React 的渲染环境的其中一种,还有 iOS 和 Android ,它们通过 React Native 渲染。(因此「虚拟 DOM」其实是个不恰当的用词)
因为协调和渲染是两个阶段,所以 React 能支持许多渲染目标。协调器会计算树的哪些部分需要更新;渲染器则通过这些信息来实际更新已渲染的应用。
这也意味着 React DOM 和 React Native 更共用同一个协调器,由 React core 提供。而 Fiber 就是这么个协调器。
调度
- 调度 scheduling - 决定工作应当何时执行。
- 工作 work - 计算工作。通常是指更新工作。
React 的 设计理念 对调度做了描述:
在 React 当前的实现中,React 在单个 tick 周期中递归地走完这棵树,然后调用整个更新后树的渲染方法。但是以后 React 可能会延迟一些更新操作来防止掉帧。
这在 React 的设计中很常见。有一些流行的库实现了 “push” 模式,即当新数据到达时再计算。然而 React 坚持 “pull” 模式,即计算可以延迟到必要时再执行。
React 不是一个常规的数据处理库,它是开发用户界面的库。我们认为 React 在一个应用中的位置很独特,它知道当前哪些计算当前是相关的,哪些不是。
如果不在当前屏幕,我们可以延迟执行相关逻辑。如果数据数据到达的速度快过帧速,我们可以合并、批量更新。我们优先执行用户交互(例如按钮点击形成的动画)的工作,延后执行相对不那么重要的后台工作(例如渲染刚从网络上下载的新内容),从而避免掉帧。
关键点如下:
- UI 中不是所有的更新都需要及时地去执行,这么做可能会导致掉帧,影响用户体验。
- 不同类型的更新有不同的优先级,比如动画更新应该比数据更新来得快。
- 开发者不需要处理调度工作,React 会聪明地处理调度。
先前的 React 还没有充分利用调度机制,而这也是 Fiber 的主要目标之一。
Fiber 是什么
Fiber 需支持:
- 暂停工作并在后续继续;
- 为不同类型的工作设置优先级;
- 重用先前已完成的工作;
- 如果工作不再需要了则抛弃工作。
为了实现这些功能,我们需要先讲工作分解为小的单元。Fiber 因此得名,一个 fiber 代表一个小的任务单元。
To go further, let's go back to the conception of React components as functions of data, commonly expressed as
v = f(d)
It follows that rendering a React app is akin to calling a function whose body contains calls to other functions, and so on. This analogy is useful when thinking about fibers.
在计算机中使用 调用栈 来追踪程序的执行过程。当一个函数被调用时,会向调用栈中添加一个新的栈帧。栈帧代表着函数的执行工作。
而对于 UI ,如果一次(函数调用)执行了过多的工作,则会导致掉帧/卡顿等问题。而且有的工作可能会因为新的状态更新而变得没有意义,但依然在执行中,阻塞了新的工作的执行。
目前浏览器提供了一些方法来解决这些问题:requestIdleCallback
能够将一个函数(通常是低优先级)调度到闲置 idle 阶段去调用,而 requestAnimationFrame
会将函数(通常是高优先级)调度到下一个动画桢里调用。但如果要使用这些方法,你就需要将渲染工作(根据优先级)进行拆分。
而 React Fiber 专为 React 组件重新“实现”了调用栈。可以将一个 fiber 视为一个虚拟的栈帧。Fiber 可以根据需要控制虚拟栈帧的执行时间和执行方式,以此来实现调度。除了调度,Fiber 还能带来 并发、错误边界 等特性。
Fiber 的结构
一个 fiber 是一个 JS 对象。它包含了一个组件自身,以及其输入输出的信息。
一个 fiber 会关联一个栈帧,和一个组件实例。
以下是 fiber 的重要字段:
type
和 key
The type and key of a fiber serve the same purpose as they do for React elements. (In fact, when a fiber is created from an element, these two fields are copied over directly.)
type
表明一个 fiber 所对应的组件类型。对于自定义组件,type
则为组件函数或类组件自身,对于内置组件,比如 div
、span
等,type
则为一个字符串。从概念上讲,type
指的就是 v = f(d)
里的 f()
。
在 type 的基础上,key
则是用来决定该 fiber 是否能被重用。
child
和 sibling
这俩字段用于指向其他的 fiber ,用于描述一颗 fiber 树。
child
会与组件的 render 结果关联,比如对于下面这个例子中,Parent
的 child
fiber 会与 Child
关联:
function Parent() {
return <Child />
}
当返回多个子组件时,sibiling
字段则派上用场:
function Parent() {
return [<Child1 />, <Child2 />]
}
子组件对应的 fiber 们会形成一个单链表。比如上面这个例子中,Child1
是头节点,并且会成为 Parent
的 child
。同时 Child1
的 sibling
是 Child2
。
回到 v = f(d)
这一公式,可以把子 fiber 理解为尾调用函数。
return
return
代表当前 fiber 执行后应当返回的 fiber 。通常可以认为是当前 fiber 的父亲 fiber 。
pendingProps
and memoizedProps
概念上讲,props 即函数的参数。fiber 会在执行之前设置好 pendingProps
,并且在执行后存储到 memoizedProps
。
当来临的 pendingProps
与 memoizedProps
一致时,则会让 fiber 直接复用上一次的执行结果。
pendingWorkPriority
pendingWorkPriority
表明了一个 fiber 工作的优先级。ReactPriorityLevel 列出了所有优先级类型。除了 0 代表 NoWork
以外,越小的数字代表越高的优先级。
调度器会通过这个优先级字段来查找下一个要执行的工作。
alternate
- flush - 通过 fiber 的结果来渲染到屏幕上。
- work-in-progress - 指 fiber 正在执行中,仍未返回结果。
在任何时候,一个组件最多会关联两个 fiber :当前的已被用来渲染的 fiber ,和一个正在执行的 fiber 。而这两个 fiber 互为对方的 alternate
。
一个 fiber 的 alternate
会通过 cloneFiber
懒创建,而非均创建一个新对象。cloneFiber
会尝试重复使用 fiber 的 alternate
(如果存在的话),以减少内存开销。
output
- host component - React 应用的叶子节点。
每一个 fiber 都会有输出 output
,但是 output
只会在叶子节点上创建,并且会沿着树向上传递。
渲染器会使用 output
来获取变更,依次来进行渲染工作。