19年大家都在问佩奇是啥,对于React来说,React 16已经出来很久了,很多人还是说不清Fiber是啥。
关于Fiber的文章汗牛充栋,从React 16.0 到React 16.4 (项目使用版本) 再到最新的 React 16.7,尽管大致框架没变,但是实现细节一直在变,在不同时间点发布的React Fiber源码间切换难免会有错乱。因此决定对Fiber运行框架基于最新React 16.7做一个大致的梳理。
源码解读分为两部分:
第一部分就是本文章,重点了解fiber的数据结构。
第二部分也已完成,重点了解fiber的运行流程:React 16.7 Fiber源码解读 (二)之运行流程
梳理过程中参考了以下文章:
A Cartoon Intro to Fiber
React Fiber Architecture
Inside Fiber: in-depth overview of the new reconciliation algorithm in React
React Fiber初探
本篇文章目录如下:
The goal of React Fiber is to increase its suitability for areas like animation, layout, and gestures. Its headline feature is incremental rendering: the ability to split rendering work into chunks and spread it out over multiple frames.
React Fiber是在render/reconciliation阶段,即协调阶段的核心调度算法进行了重构。commit阶段还是同步的,不允许打断。
为了配合这次重构,从React 15 到React 16,协调阶段的生命周期函数也发生了重大变化,具体可以参考我之前的一篇博文:
React v15到v16.3, v16.4新生命周期总结以及使用场景
Reconciler即协调算法,用于计算新老View的差异。React 16之前的reconciler叫Stack reconciler。Fiber是React的新reconciler。Renderer则是和平台相关的代码,负责将View的变化渲染到不同的平台上,DOM、Canvas、Native、VR、WebGL等等平台都有自己的renderer。我们可以看出reconciler是React的核心代码,是各个平台共用的。因此这次React的reconciler更新到Fiber架构是一次重量级的核心架构的更换。
React 16 之前的调度算法被称为Stack Reconciler,即递归遍历所有的 Virtual DOM 节点,进行 Diff,一旦开始无法中断,要等整棵 Virtual DOM 树计算完成之后,才会释放主线程。而浏览器中的渲染引擎和js引擎是互斥的,Diff的过程中动画等周期性任务无法立即得到处理,就会出现卡顿即掉帧,影响用户体验。
React16 采用增量渲染(incremental rendering)也即异步渲染(async rendering)用来解决掉帧的问题,将渲染任务拆分成多个小任务,每次只做一个小任务,做完后就把时间控制权交还给主线程去执行优先级更高的任务(动画,交互等),而不像之前长时间占用。
16.7尚未启动异步渲染
We think async rendering is a big deal, and represents the future of React. To make migration to v16.0 as smooth as possible, we’re not enabling any async features yet, but we’re excited to start rolling them out in the coming months. Stay tuned!
来自React 16: A look inside an API-compatible rewrite of our frontend UI library
We’re excited to announce that with the release of React 16, we’re completely switching over to the new implementation of React. Even though React’s new core is designed to support asynchrony, we’re currently running React in a synchronous mode compatible with our older code. This allowed us to move to the new implementation smoothly. As with most of our releases, we’ve been testing the new code in production for months so we feel optimistic that people will be able to adopt it without running into issues.
从以上两段英文摘要均来自权威的官方,可见当前React 16的状态是(React 16.7已验证):尽管React 16已经使用了Fiber架构,但是为了从16到17的平滑过渡以及新架构还在验证测试阶段,异步渲染并没有开启,还是采用同步渲染。
那么问题来了:我们如何体验异步渲染的效果?
在React 16的早期版本中,开启异步渲染的方式是ReactDOM.unstable_deferredUpdates方法,包括官方提供的那个fiber demo,都用的这个方法。
componentDidMount() {
this.invervalID = setInterval(this.tick, 1000);
}
tick = () => {
ReactDOMFiber.unstable_deferredUpdates(() =>
this.setState(state => ({seconds: (state.seconds % 10) + 1}))
);
}
但从某个版本开始,这个方法由于一些潜在问题被移除了,目前官方建议直接采用requestIdleCallback来降低某个可能耗时操作的优先级。
Remove buggy unstable_deferredUpdates() #13488
我自己在Github上fork了一个Fiber Demo, 有兴趣的话可以下载验证
GitHub - Fiber Demo项目 (React 16.0)
内容是一个具有动画的三角形,里面的数字从0到10循环更新,动画和数字更新间有主线程争夺的冲突。
线上Fiber Demo
线上stack demo
对比可以发现,React 15中掉帧非常严重,React 16中采用了unstable_deferredUpdates就很流畅了。
requestIdleCallback: 在线程空闲时期调度执行低优先级函数(可能会隔几帧)
requestAnimationFrame: 在下一个动画帧调度前执行高优先级函数;
Fiber是将渲染任务分片,然后根据不同的优先级使用以上API调度,异步执行指定任务。
requestIdleCallback方法提供deadline,即限制任务执行时间,以切分任务,同时避免任务长时间执行,阻塞UI渲染而导致掉帧;
requestIdleCallback(lowPriorityWork, { timeout: 1000 });
function lowPriorityWork (deadline) {
// 该帧尚有空闲时间或该任务已经timeout
while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
tasks.length > 0) {
runTask();
}
if (tasks.length > 0) {
requestIdleCallback(lowPriorityWork);
}
}
并不是所有的浏览器都支持requestIdleCallback,但是React内部实现了自己的polyfill,所以不必担心浏览器兼容性问题
Fiber对象
export type Fiber = {|
// Tag identifying the type of fiber.
tag: WorkTag,
// The value of element.type which is used to preserve the identity during
// reconciliation of this child.
elementType: any,
// The resolved function/class/ associated with this fiber.
type: any,
// The local state associated with this fiber.
stateNode: any,
// 当前fiber的父级fiber实例
return: Fiber | null,
// 子Fiber
child: Fiber | null,
// 兄弟fiber
sibling: Fiber | null,
index: number,
// 当前处理过程中的组件props对象
pendingProps: any,
// 缓存的之前组件props对象, 便于恢复
memoizedProps: any,
// A queue of state updates and callbacks.
// 组件状态更新和对应回调函数的存储队列
updateQueue: UpdateQueue<any> | null,
// 缓存的之前组件state对象, 便于恢复
memoizedState: any,
mode: TypeOfMode,
// Effect
effectTag: SideEffectTag,
// Singly linked list fast path to the next fiber with side-effects.
nextEffect: Fiber | null,
// The first and last fiber with side-effect within this subtree. This allows
// us to reuse a slice of the linked list when we reuse the work done within
// this fiber.
firstEffect: Fiber | null,
lastEffect: Fiber | null,
// Represents a time in the future by which this work should be completed.
// 本质上是优先级
expirationTime: ExpirationTime,
// This is used to quickly determine if a subtree has no pending changes.
childExpirationTime: ExpirationTime,
// This is a pooled version of a Fiber. Every fiber that gets updated will
// eventually have a pair. There are cases when we can clean up pairs to save
// memory if we need to.
alternate: Fiber | null,
// ... other fields
|};
Fiber对象中最重要的是return, child和sibling,正是通过这3个属性才构成了一颗fiber树。
mode: ReactTypeOfMode
Bitfield that describes properties about the fiber and its subtree. E.g.
the ConcurrentMode flag indicates whether the subtree should be async-by-
default. When a fiber is created, it inherits the mode of its
parent. Additional flags can be set at creation time, but after that the
value should remain unchanged throughout the fiber’s lifetime, particularly
before its child fibers are created.
ReactTypeOfMode.js
export type TypeOfMode = number;
export const NoContext = 0b000; // 同步模式
export const ConcurrentMode = 0b001; // 异步模式,以前叫AsyncMode
export const StrictMode = 0b010;
export const ProfileMode = 0b100;
在React中,Dom节点state和props的改变导致视图改变的操作称为side effect.
effectTag就是记录这种操作,在源码中以二进制的形式保存,因此可以记录多种操作。
So effects in Fiber basically define the work that needs to be done for instances after updates have been processed. For host components (DOM elements) the work consists of adding, updating or removing elements. For class components React may need to update refs and call the componentDidMount and componentDidUpdate lifecycle methods. There are also other effects corresponding to other types of fibers.
ReactSideEffectTags.js
export type SideEffectTag = number;
// 二进制形式,因此可累加
export const NoEffect = 0
export const PerformedWork = 1
export const Placement = 2;
export const Update = 4;
export const PlacementAndUpdate = 6;
export const Deletion = 8;
export const ContentReset = 16;
export const Callback = 32;
export const DidCapture = 64;
export const Ref = 128;
export const Snapshot =256;
// ...;
React Fiber提供了一个叫做effect list的数组,包含了需要改变的react element对应的fiber对象(firstEffect,lastEffect)。effect list数据结构的好处是可以快速拿到状态改变的Dom而不必遍历整个React Root.
哪些element需要做insert, update或者delete,又或者哪个element需要调用周期函数。这些都通过effect list的item反映出来。
举个栗子:
下图代表一个React项目的fiber树。其中橘黄色的节点代表发生了side effect,需要进行相应的操作(work):
c2是带插入(insert)节点,d2和c1改变属性(attribute),b2需要触发一个周期(life cycle)函数。
对应的effect list如下:
ReactFiberExpirationTime.js
expirationTime本质上是fiber work执行的优先级。
关于fiber work的优先级问题,从React 16.0 到 React 16.7 有一些重大的改动
最早是将Priority划分为5级
{
NoWork: 0, // No work is pending.
SynchronousPriority: 1, // For controlled text inputs. Synchronous side-effects.
TaskPriority: 2, // Completes at the end of the current tick.
HighPriority: 3, // Interaction that needs to complete pretty soon to feel responsive.
LowPriority: 4, // Data fetching, or result from updating stores.
OffscreenPriority: 5, // Won't be visible but do the work in case it becomes visible.
}
这样划分的缺点时粒度不够细,后来又换成了基于时间的分级机制:
Sync 代表同步模式,立即处理,优先级最高,为1.
Never代表用不执行,优先级最低,为32位浮点数最大值。
export const NoWork = 0; // 没有任务等待处理
export const Sync = 1; // 同步模式,立即处理任务
export const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1
...
但是当我阅读源码的时候,又发生了变化,主要是代表Never和Sync的数值发生了翻转。算法不变,但需要注意一下。
export const NoWork = 0;
export const Never = 1;
export const Sync = MAX_SIGNED_31_BIT_INT;
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;
// ...
expirationTime的计算方式如下:
ReactFiberScheduler.js
let expirationContext: ExpirationTime = NoWork;
let nextRenderExpirationTime: ExpirationTime = NoWork;
let nextFlushedExpirationTime: ExpirationTime = NoWork;
let lowestPriorityPendingInteractiveExpirationTime: ExpirationTime = NoWork;
// 程序启动时设置, 为performance.now()
// performance.now() 输出的是相对于 performance.timing.navigationStart(页面初始化) 的时间
let originalStartTimeMs: number = now();
let currentRendererTime: ExpirationTime = msToExpirationTime(
originalStartTimeMs,
);
let currentSchedulerTime: ExpirationTime = currentRendererTime;
// 根据不同的阶段设置fiber任务的优先级
function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {
let expirationTime;
if (expirationContext !== NoWork) {
expirationTime = expirationContext;
} else if (isWorking) { // fiber任务是否到期
if (isCommitting) {
// 提交阶段(phase 2)设置为同步优先级即优先级最高
expirationTime = Sync;
} else {
// 渲染阶段(phase 1: render phase)发生的更新优先级设置为下次渲染的到期时间
expirationTime = nextRenderExpirationTime;
}
} else {
// fiber任务没有到期则重新计算expiration
// 异步模式
if (fiber.mode & ConcurrentMode) {
if (isBatchingInteractiveUpdates) {
// This is an interactive update
expirationTime = computeInteractiveExpiration(currentTime);
} else {
// This is an async update
expirationTime = computeAsyncExpiration(currentTime);
}
// If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.
if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
expirationTime -= 1;
}
} else {
// 同步模式
expirationTime = Sync;
}
}
// ...
return expirationTime;
}
computeInteractiveExpiration 和 computeAsyncExpiration 的实现都是在
ReactFiberExpirationTime.js
import MAX_SIGNED_31_BIT_INT from './maxSigned31BitInt';
export type ExpirationTime = number;
export const NoWork = 0;
export const Never = 1;
// 1073741823
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
export const Sync = MAX_SIGNED_31_BIT_INT;
// ms 分片
const UNIT_SIZE = 10;
// 异步任务优先级上限,为Sync-1
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;
// 1 unit of expiration time represents 10ms.
export function msToExpirationTime(ms: number): ExpirationTime {
// |0为去掉小数位。
return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}
export function expirationTimeToMs(expirationTime: ExpirationTime): number {
return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;
}
// 返回距离num最近的precision的倍数
function ceiling(num: number, precision: number): number {
// ceiling(1010, 20) return: 1020
// ceiling(90, 20) return 100
return (((num / precision) | 0) + 1) * precision;
}
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return (
MAGIC_NUMBER_OFFSET -
ceiling(
MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
export function computeAsyncExpiration(
currentTime: ExpirationTime,
): ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
);
}
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
export function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
}
通过源码我们可以发现:computeInteractiveExpiration 和 computeAsyncExpiration最终调用的都是computeExpirationBucket,所不同的只是后两个参数。interative的优先级是高于Async的
alternate : Fiber || null
一个React组件有current fiber和alternate fiber,alternate fiber也被称为work in progress fiber
当组件第一次render时构建的fiber tree为current,接下来当组件状态改变时新构建的fiber tree称为work in progress,代表将来的新状态。
这两个fiber都有一个更新队列(UpdateQueue)。队列中item的引用是相同的,区别在于,work in progress fiber会在从队列中移除更新好的item。因此working in progress中的updateQueue是current fiber updateQueue的一个子集
所有更新完成之后,working in progress fiber成为新的current fiber。如果更新中断或失败,current fiber可以用来恢复
updateQueue是一个单向链表,firstUpdate和lastUpdate分别指向链表的头部和尾部。记录fiber对应的React Element的state变化
updateQueue: UpdateQueue | null
ReactUpdateQueue.js
export type UpdateQueue<State> = {
baseState: State,
firstUpdate: Update<State> | null,
lastUpdate: Update<State> | null,
firstCapturedUpdate: Update<State> | null,
lastCapturedUpdate: Update<State> | null,
firstEffect: Update<State> | null,
lastEffect: Update<State> | null,
firstCapturedEffect: Update<State> | null,
lastCapturedEffect: Update<State> | null,
};
下图是一个真实的updateQueue,用户有个操作是将下拉框的值从entropy变为gini。
Fiber数据结构就先到这里了,下一篇我会梳理一下Fiber运行流程,敬请期待:)