小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
为什么有Fiber
在 react15 之前,如果页面比较复杂,元素很多,而且又有频繁刷新 state
的时候,页面会出现不流畅、掉帧卡顿的情况。究其原因就是,由于 JS
是单线程的,大量的同步运算阻塞了页面的 UI
渲染。当 React 调用 setState
的时候,React 会遍历应用所有节点,计算出需要更新的差异点,然后更新 UI
。整个过程是一步到位的,中间不能被打断,所以如果元素过多的时候就会出现卡顿的情况。
什么是Fiber
通常为了解决“同步阻塞”问题,一般都是有两种方案:异步或者分片处理(任务拆解)。React
为了解决“同步阻塞”导致的卡顿问题,选用 分片处理 这种方案,这也就是我们所说的 Fiber
架构。
Fiber
是对 React 核心算法的重构,主要就是将原来的 Stack Reconciler
替换为 Fiber Reconciler
。
Fiber 的目标主要有5个:
- 能够把可中断的任务切片处理。
- 能够调整优先级,重置并复用任务。
- 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
- 能够在 render() 中返回多个元素。
- 更好地支持错误边界。
PS: React 代码库概述
单链表树结构
Singly Linked List Tree Structure
Fiber
的数据结构特点是它本身是一个单链表树结构,可以进行快速的插入和删除操作。 类型定义的部分源码如下:
export type Fiber = {
// Tag identifying the type of fiber.
tag: WorkTag,
// Unique identifier of this child.
key: null | string,
// 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,
// Conceptual aliases
// parent : Instance -> return The parent happens to be the same as the
// return fiber since we've merged the fiber and instance.
// Remaining fields belong to Fiber
// The Fiber to return to after finishing processing this one.
// This is effectively the parent, but there can be multiple parents (two)
// so this is only the parent of the thing we're currently processing.
// It is conceptually the same as the return address of a stack frame.
return: Fiber | null,
// Singly Linked List Tree Structure.
child: Fiber | null,
sibling: Fiber | null,
index: number,
...
// 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,
lanes: Lanes,
childLanes: Lanes,
// 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,
...
}
PS:详情查看(https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js
)
Fiber节点构造函数
Fiber节点的格式主要包含
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag;
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;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// Effects
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
this.lanes = NoLanes;
this.childLanes = NoLanes;
this.alternate = null;
...
}
PS:详情查看(https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiber.new.js
)
实现过程
Fiber Reconciler
分为两个阶段执行。
1. render/reconciliation
render/reconciliation
生成 fiber
树,得到需要更新的差异点,这个过程是可以被打断的,
这里用到了双缓冲池技术,Fiber
最多只要树的两个版本。在这个阶段,除了生成 fiber tree
, 在 diff
过程中构建 workInProgress tree
,每一个 fiber
节点都要一个 alternate
属性(本身也是一个 fiber 结构),创建workInProgress
时候优先使用current.alternate
,没有的话再去创建。
workInProgress tree
的目的就是为了能够 复用(reuse
)fiber。
Fiber Reconciler
以 fiber
为工作单元,从上而下开始构建 workInProgress tree
。
具体过程:
- 如果当前节点不需要更新,直接把子节点clone过来,跳到 5;要更新的话打个 tag
- 更新当前节点状态(props, state, context等)
- 调用 shouldComponentUpdate(),false的话,跳到 5
- 调用 render() 获得新的子节点,并为子节点创建fiber(创建过程会尽量复用现有fiber,子节点增删也发生在这里)
- 如果没有产生child fiber,该工作单元结束,把effect list归并到return,并把当前节点的sibling作为下一个工作单元;否则把child作为下一个工作单元
- 如果没有剩余可用时间了,等到下一次主线程空闲时才开始下一个工作单元;否则,立即开始做
- 如果没有下一个工作单元了(回到了workInProgress tree的根节点),第1阶段结束,进入pendingCommit状态
ps:参考(www.ayqy.net/blog/dive-i…)
构建 workInProgress tree
的过程就是 diff
的过程,通过 requestIdleCallback
来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),每完成一组任务,把时间控制权交还给主线程,直到下一次 requestIdleCallback
回调再继续构建 workInProgress tree
window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间
为了实现上面的分组执行任务过程,React 有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:
- synchronous,与之前的Stack Reconciler操作一样,同步执行
- task,在next tick之前执行
- animation,下一帧之前执行
- high,在不久的将来立即执行
- low,稍微延迟执行也没关系
- offscreen,下一次render时或scroll时才执行
synchronous
首屏(首次渲染)用,要求尽量快,不管会不会阻塞UI线程。animation
通过 requestAnimationFrame
来调度,这样在下一帧就能立即开始动画过程;后3个都是由 requestIdleCallback
回调执行的;offscreen
指的是当前隐藏的、屏幕外的(看不见的)元素
优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。
workInProgress tree
构建完毕,得到的就是新的fiber tree
,然后把 current
指针指向 workInProgress tree
,丢掉旧的fiber tree
.
部分源码如下:
// This is used to create an alternate fiber to do work on.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
let workInProgress = current.alternate;
if (workInProgress === null) {
// We use a double **buffering pooling technique** because we know that we'll
// only ever need at most two versions of a tree. We pool the "other" unused
// node that we're free to reuse. This is lazily created to avoid allocating
// extra objects for things that are never updated. It also allow us to
// reclaim the extra memory if needed.
workInProgress = createFiber(
current.tag,
pendingProps,
current.key,
current.mode,
);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
// fiber 与 workInProgress 互相持有引用
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
workInProgress.pendingProps = pendingProps;
// Needed because Blocks store data on type.
workInProgress.type = current.type;
// We already have an alternate.
// Reset the effect tag.
workInProgress.flags = NoFlags;
// The effects are no longer valid.
workInProgress.subtreeFlags = NoFlags;
workInProgress.deletions = null;
if (enableProfilerTimer) {
// We intentionally reset, rather than copy, actualDuration & actualStartTime.
// This prevents time from endlessly accumulating in new commits.
// This has the downside of resetting values for different priority renders,
// But works for yielding (the common case) and should support resuming.
workInProgress.actualDuration = 0;
workInProgress.actualStartTime = -1;
}
}
// Reset all effects except static ones.
// Static effects are not specific to a render.
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue;
// Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
const currentDependencies = current.dependencies;
workInProgress.dependencies =
currentDependencies === null
? null
: {
lanes: currentDependencies.lanes,
firstContext: currentDependencies.firstContext,
};
// These will be overridden during the parent's reconciliation
workInProgress.sibling = current.sibling;
workInProgress.index = current.index;
workInProgress.ref = current.ref;
if (enableProfilerTimer) {
workInProgress.selfBaseDuration = current.selfBaseDuration;
workInProgress.treeBaseDuration = current.treeBaseDuration;
}
return workInProgress;
}
在这个阶段会被执行的生命周期包含:
componentWillMount(已废弃)
componentWillReceiveProps(已废弃)
getDerivedStateFromProps
shouldComponentUpdate
componentWillUpdate (已废弃)
2. commit
commit
批量更新差异点 (effect list
),这个过程不能被打断。
在这个阶段执行的生命周期:
componentDidMount
componentDidUpdate
componentWillUnmount
fiber树结构图
(图片来源于网络)
常见问题
fiber 如何中断/断点恢复?
中断:检查当前正在处理的工作单元,保存当前成果(firstEffect
, lastEffect
),修改 tag
标记一下,迅速收尾并再开一个requestIdleCallback
,下次有机会再做
断点恢复:下次再处理到该工作单元时,看 tag
是被打断的任务,接着做未完成的部分或者重做
结语
如果这篇文章帮到了你,欢迎点赞👍和关注⭐️。
文章如有错误之处,希望在评论区指正🙏🙏。
附: