一. Fiber是什么
在fiber出现之前,react的架构体系只有协调器reconciler和渲染器render。
当前有新的update时,react会递归所有的vdom节点,如果dom节点过多,会导致其他事件影响滞后,造成卡顿。即之前的react版本无法中断工作过程,一旦递归开始无法停留下来。
为了解决这一系列问题,react在v16版本重构了底层架构,引入了fiber,fiber本质上是一种数据结构。 fiber的出现使得react能够异步可中断工作任务,并且可以在浏览器空闲时,从中断处继续往下工作。 当出现多个高优先任务并行时,react引入lane模型取代之前的expireTime机制。
Stack Reconciler 是 React v15 及之前版本使用的协调算法。而 React Fiber 则是从 v16 版本开始对 Stack Reconciler 进行的重写,是 v16 版本的核心算法实现。 Stack Reconciler 的实现使用了同步递归模型,该模型依赖于内置堆栈来遍历。React 团队 Andrew 之前有提到:
Stack Reconciler 天生带来的局限性,使得 DOM 更新过程是同步的。也就是说,在虚拟 DOM 的比对过程中,如果发现一个元素实例有更新,则会立即同步执行操作,提交到真实 DOM 的更改。这在动画、布局以及手势等领域,可能会带来非常糟糕的用户体验。因此,为了解决这个问题,React 实现了一个虚拟堆栈帧。实际上,这个所谓的虚拟堆栈帧本质上是建立了多个包含节点和指针的链表数据结构。每一个节点就是一个 fiber 基本单元,这个对象存储了一定的组件相关的数据域信息。而指针的指向,则是串联起整个 fibers 树。重新自定义堆栈带来显而易见的优点是,可以将堆栈保留在内存中,在需要执行的时候执行它们,这使得暂停遍历和停止堆栈递归成为可能。
二.基础概念
Work
在 React Reconciliation 过程中出现的各种必须执行计算的活动,比如 state update,props update 或 refs update 等,这些活动我们可以统一称之为 work。
Fiber 对象
文件位置:packages/react-reconciler/src/ReactFiber.old.js
每一个 React 元素对应一个 fiber 对象,一个 fiber 对象通常是表征 work 的一个基本单元。fiber 对象有几个属性,这些属性指向其他 fiber 对象。
- child: 对应于父 fiber 节点的子 fiber
- sibling: 对应于 fiber 节点的同类兄弟节点
- return: 对应于子 fiber 节点的父节点
因此 fibers 可以理解为是一个包含 React 元素上下文信息的数据域节点,以及由 child, sibling 和 return 等指针域构成的链表结构。
fiber 对象主要的属性如下所示:
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
// 作为静态数据结构的属性
this.tag = tag; // 标识 fiber 类型的标签
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null;
// Fiber
this.return = null; // 指向父节点
this.child = null; // 指向子节点
this.sibling = null; // 指向兄弟节点
this.index = 0;
this.ref = null;
// 作为动态的工作单元的属性
this.pendingProps = pendingProps; // 在开始执行时设置 props 值
this.memoizedProps = null; // 在结束时设置的 props 值
this.updateQueue = null;
this.memoizedState = null; // 当前 state
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
if (enableProfilerTimer) {
// Note: The following is done to avoid a v8 performance cliff.
this.actualDuration = Number.NaN;
this.actualStartTime = Number.NaN;
this.selfBaseDuration = Number.NaN;
this.treeBaseDuration = Number.NaN;
this.actualDuration = 0;
this.actualStartTime = -1;
this.selfBaseDuration = 0;
this.treeBaseDuration = 0;
}
if (__DEV__) {
// This isn't directly used but is handy for debugging internals:
this._debugSource = null;
this._debugOwner = null;
this._debugNeedsRemount = false;
this._debugHookTypes = null;
if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
Object.preventExtensions(this);
}
}
}
这里需要提一下,为什么父级指针叫做
return
而不是parent
或者father
呢?因为作为一个工作单元,return
指节点执行完completeWork
(本章后面会介绍)后会返回的下一个节点。子Fiber节点
及其兄弟节点完成工作后会返回其父级节点,所以用return
指代父级节点。
从React元素创建一个fiber对象
文件位置 packages/react-reconciler/src/ReactFiber.old.js
export function createFiberFromElement(
element: ReactElement,
mode: TypeOfMode,
lanes: Lanes,
): Fiber {
let owner = null;
if (__DEV__) {
owner = element._owner;
}
const type = element.type;
const key = element.key;
const pendingProps = element.props;
const fiber = createFiberFromTypeAndProps(
type,
key,
pendingProps,
owner,
mode,
lanes,
);
if (__DEV__) {
fiber._debugSource = element._source;
fiber._debugOwner = element._owner;
}
return fiber;
}
Reconciliation 和 Scheduling
协调(Reconciliation):
简而言之,根据 diff 算法来比较虚拟 DOM,从而可以确认哪些部分的 React 元素需要更改。
调度(Scheduling):
可以简单理解为是一个确定在什么时候执行 work 的过程。
Render 阶段和 Commit 阶段
这是 React 团队作者 Dan Abramov 画的一张生命周期阶段图,详情点击查看。他把 React 的生命周期主要分为两个阶段:render 阶段和 commit 阶段。其中 commit 阶段又可以细分为 pre-commit 阶段和 commit 阶段,如下图所示:
在 render 阶段,React 可以根据当前可用的时间片处理一个或多个 fiber 节点,并且得益于 fiber 对象中存储的元素上下文信息以及指针域构成的链表结构,使其能够将执行到一半的工作保存在内存的链表中。当 React 停止并完成保存的工作后,让出时间片去处理一些其他优先级更高的事情。之后,在重新获取到可用的时间片后,它能够根据之前保存在内存的上下文信息通过快速遍历的方式找到停止的 fiber 节点并继续工作。由于在此阶段执行的工作并不会导致任何用户可见的更改,因为并没有被提交到真实的 DOM。所以,我们说是 fiber 让调度能够实现暂停、中止以及重新开始等增量渲染的能力。相反,在 commit 阶段,work 执行总是同步的,这是因为在此阶段执行的工作将导致用户可见的更改。这就是为什么在 commit 阶段, React 需要一次性提交并完成这些工作的原因。
Effects list
effect list 可以理解为是一个存储 effectTag 副作用列表容器。它是由 fiber 节点和指针 nextEffect 构成的单链表结构,这其中还包括第一个节点 firstEffect,和最后一个节点 lastEffect。如下图所示:
React 采用深度优先搜索算法,在 render 阶段遍历 fiber 树时,把每一个有副作用的 fiber 筛选出来,最后构建生成一个只带副作用的 effect list 链表。
在 commit 阶段,React 拿到 effect list 数据后,通过遍历 effect list,并根据每一个 effect 节点的 effectTag 类型,从而对相应的 DOM 树执行更改。
Current 树和 WorkInProgress 树
首次渲染之后,React 会生成一个对应于 UI 渲染的 fiber 树,称之为 current 树。实际上,React 在调用生命周期钩子函数时就是通过判断是否存在 current 来区分何时执行 componentDidMount 和 componentDidUpdate。当 React 遍历 current 树时,它会为每一个存在的 fiber 节点创建了一个替代节点,这些节点构成一个 workInProgress 树。后续所有发生 work 的地方都是在 workInProgress 树中执行,如果该树还未创建,则会创建一个 current 树的副本,作为 workInProgress 树。当 workInProgress 树被提交后将会在 commit 阶段的某一子阶段被替换成为 current 树。
这里增加两个树的主要原因是为了避免更新的丢失。比如,如果我们只增加更新到 workInProgress 树,当 workInProgress 树通过从 current 树中克隆而重新开始时,一些更新可能会丢失。同样的,如果我们只增加更新到 current 树,当 workInProgress 树被提交后会被替换为 current 树,更新也会被丢失。通过在两个队列都保持更新,可以确保更新始终是下一个 workInProgress 树的一部分。并且,因为 workInProgress 树被提交成为 current 树,并不会出现相同的更新而被重复应用两次的情况。
在React
中最多会同时存在两棵Fiber树
。当前屏幕上显示内容对应的Fiber树
称为current Fiber树
,正在内存中构建的Fiber树
称为workInProgress Fiber树
。
current Fiber树
中的Fiber节点
被称为current fiber
,workInProgress Fiber树
中的Fiber节点
被称为workInProgress fiber
,他们通过alternate
属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React
应用的根节点通过使current
指针在不同Fiber树
的rootFiber
间切换来完成current Fiber
树指向的切换。
即当workInProgress Fiber树
构建完成交给Renderer
渲染在页面上后,应用根节点的current
指针指向workInProgress Fiber树
,此时workInProgress Fiber树
就变为current Fiber树
。
每次状态更新都会产生新的workInProgress Fiber树
,通过current
与workInProgress
的替换,完成DOM
更新。
mount时
function App() {
const [num, add] = useState(0);
return (
<p onClick={() => add(num + 1)}>{num}</p>
)
}
ReactDOM.render(<App/>, document.getElementById('root'));
- 首次执行
ReactDOM.render
会创建fiberRootNode
(源码中叫fiberRoot
)和rootFiber
。其中fiberRootNode
是整个应用的根节点,rootFiber
是<App/>
所在组件树的根节点。
之所以要区分fiberRootNode
与rootFiber
,是因为在应用中我们可以多次调用ReactDOM.render
渲染不同的组件树,他们会拥有不同的rootFiber
。但是整个应用的根节点只有一个,那就是fiberRootNode
。
fiberRootNode
的current
会指向当前页面上已渲染内容对应Fiber树
,即current Fiber树
。
fiberRootNode.current = rootFiber;
由于是首屏渲染,页面中还没有挂载任何DOM
,所以fiberRootNode.current
指向的rootFiber
没有任何子Fiber节点
(即current Fiber树
为空)。
- 接下来进入
render阶段
,根据组件返回的JSX
在内存中依次创建Fiber节点
并连接在一起构建Fiber树
,被称为workInProgress Fiber树
。(下图中右侧为内存中构建的树,左侧为页面显示的树)
在构建workInProgress Fiber树
时会尝试复用current Fiber树
中已有的Fiber节点
内的属性,在首屏渲染
时只有rootFiber
存在对应的current fiber
(即rootFiber.alternate
)。
3. 图中右侧已构建完的workInProgress Fiber树
在commit阶段
渲染到页面。
此时DOM
更新为右侧树对应的样子。fiberRootNode
的current
指针指向workInProgress Fiber树
使其变为current Fiber 树
。
update时
- 接下来我们点击
p节点
触发状态改变,这会开启一次新的render阶段
并构建一棵新的workInProgress Fiber 树
。
和mount
时一样,workInProgress fiber
的创建可以复用current Fiber树
对应的节点数据。
workInProgress Fiber 树
在render阶段
完成构建后进入commit阶段
渲染到页面上。渲染完毕后,workInProgress Fiber 树
变为current Fiber 树
。
requestIdleCallback
客户端线程执行任务时会以帧的形式划分,在两个执行帧之间,主线程通常会有一小段空闲时间,在这个空闲期触发 requestIdleCallback 方法,能够执行一些优先级较低的 work。
据说在早期的 React 版本上确实是这么做的,但使用 requestIdleCallback 实际上有一些限制,执行频次不足,以致于无法实现流畅的 UI 渲染,扩展性差。因此 React 团队放弃了 requestIdleCallback 用法,实现了自定义的版本。比如,在发布 v16.10 版本中,推出实验性的 Scheduler,尝试使用 postMessage 来代替 requestAnimationFrame。更多了解可以查看 React 源码 packages/scheduler 部分。
总结
Fiber 由来已久,可以说是 React 设计思想的一个典型表现。
本文简单介绍了Fiber
的基础理念,以及Fiber树
的构建与替换过程,这个过程伴随着DOM
的更新。