认识
一、认识
React Fiber
是 React
内部定义的一种数据结构, 是 Fiber
树结构的节点单位, 也就是 React 16
新架构下的虚拟 DOM
。Fiber
包含了元素信息、更新队列、类型、标记等。
React Fiber
解决了在 React 16
之前, 渲染任务都是同步的, 不可中断。这会在进行大量节点的reconcile
时可能产生卡顿,因为浏览器所有的时间都交给了JS
执行,并且JS
的执行是单线程。React16
之后就有了scheduler
进行时间片的调度,给每个任务(工作单元)一定的时间,如果在这个时间内没有执行完,也要交出执行权给浏览器进行绘制和重排。所以异步可中断的更新需要一定的数据结构在内存中保存工作单元的信息。通过 Fiber
架构, React
得以实现了将不可中断的同步更新改为可中断的异步更新。
二、工作
-
作为静态数据结构: 每个
Fiber
节点对应一个React element
,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的DOM
节点等信息。 -
作为动态工作单元: 每个
Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作。updateQueue
存储了更新队列,flags
存储了要执行的操作
三、结构
Fiber
对象上面保存了包括这个节点的属性、类型、dom
等,Fiber
通过child
、sibling
、return
(指向父节点)来形成Fiber
树,还保存了更新状态时用于计算state
的updateQueue
,updateQueue
是一种链表结构,上面可能存在多个未计算的update
,update
也是一种数据结构,上面包含了更新的数据、优先级等,除了这些之外,上面还有和副作用有关的信息。
export class FiberNode {
constructor(tag, pendingProps, key) {
// 存储节点的类型、实例、状态
this.tag = tag;
this.key = key;
this.type = null;
this.stateNode = null;
// 存储节点的父节点、子节点、兄弟节点信息
this.return = null;
this.sibling = null;
this.child = null;
this.index = 0;
// 存储节点的 ref 信息
this.ref = null;
this.refCleanup = null;
// 存储节点的 props、state、更新队列、依赖项信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.memoizedState = null;
this.updateQueue = null;
this.dependencies = null;
// 存储节点的双缓存信息
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
// 存储节点的优先级、过期时间、双缓存信息
this.lanes = null;
this.childLanes = null;
// 存储节点的双缓存信息
this.alternate = null;
}
}
3.1 作为架构来说
作为架构来说,之前React15
的Reconciler
采用递归的方式执行,数据保存在递归调用栈中,所以被称为Stack Reconciler
。React16
的Reconciler
基于Fiber
节点实现,被称为Fiber Reconciler
。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 用于连接其他Fiber节点形成Fiber树
this.return = null; // 指向父级Fiber节点
this.child = null; // 指向子Fiber节点
this.sibling = null; // 指向右边第一个兄弟Fiber节点
this.index = 0;
}
为什么父级指针叫做return
而不是parent
或者father
呢?
答: 因为作为一个工作单元,return
指节点执行完completeWork
后会返回的下一个节点。子Fiber
节点及其兄弟节点完成工作后会返回其父级节点,所以用return
指代父级节点。
3.2 作为静态的数据结构来说
作为静态的数据结构来说,每个Fiber
节点对应一个React element
,保存了该组件的类型(函数组件/类组件/原生组件...)、对应的DOM节点等信息。
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
this.tag = tag; // Fiber对应组件的类型 Function/Class/Host...
this.key = key; // key属性
this.elementType = null; // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
this.type = null; // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.stateNode = null; // Fiber对应的真实DOM节点
}
fiber.tag
与React Element
对应关系如下:
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const IndeterminateComponent = 2; // 初始化的时候不知道是函数组件还是类组件
export const HostRoot = 3; // Root Fiber 可以理解为根元素 , 通过reactDom.render()产生的根元素
export const HostPortal = 4; // 对应 ReactDOM.createPortal 产生的 Portal
export const HostComponent = 5; // dom 元素 比如 <div>
export const HostText = 6; // 文本节点
export const Fragment = 7; // 对应 <React.Fragment>
export const Mode = 8; // 对应 <React.StrictMode>
export const ContextConsumer = 9; // 对应 <Context.Consumer>
export const ContextProvider = 10; // 对应 <Context.Provider>
export const ForwardRef = 11; // 对应 React.ForwardRef
export const Profiler = 12; // 对应 <Profiler/ >
export const SuspenseComponent = 13; // 对应 <Suspense>
export const MemoComponent = 14; // 对应 React.memo 返回的组件
3.3 作为动态的工作单元来说
作为动态的工作单元来说,每个Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新...)
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为动态的工作单元的属性
// 保存本次更新造成的状态改变相关信息
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
// 保存本次更新会造成的DOM操作
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
}
3.4 Fiber 其他结构属性
此外,还有调度优先级相关
、指向该fiber在另一次更新时对应的fiber
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// 指向该fiber在另一次更新时对应的fiber
this.alternate = null;
}
四、双缓存
所谓双缓存是指在内存中构建并直接替换的技术。Fiber双缓存
指的就是: 在经过Reconcile(diff)
形成了新的workInProgress Fiber
然后将workInProgress Fiber
切换成current Fiber
应用到真实dom
中。存在双Fiber
的好处是在内存中形成视图的描述,在最后应用到dom中,减少了对dom
的操作。
4.1 Fiber两棵树
在React
中最多会从同时存在两颗Fiber
树。当前屏幕上显示(呈现)的内容对应的Fiber
树称为current Fiber
树,正在内存中构建的Fiber
树称为workInProgress Fiber
树。
4.2 Fiber树连接
current Fiber
树中的Fiber
节点被称为current fiber
,workInProgress Fiber
树中的Fiber
节点被称为workInProgress fiber
,他们通过alternate
属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
4.3 Fiber树切换
React
应用的根节点通过使current
指针在不同Fiber
树的rootFiber间切换来完成
current Fiber树指向的切换。即当
workInProgress Fiber树构建完成交给
Renderer渲染在页面上后,应用根节点的
current指针指向
workInProgress Fiber树,此时
workInProgress Fiber树就变为
current Fiber`树。
4.4 Fiber 双缓存过程
-
mount 阶段
-
首次执行
React.render()
或者ReactDOM.unstable_createRoot
时,创建fiberRoot
和rootFiber
-
fiberRoot
: 整个应用的根节点,整个应用的根节点只有一个 -
rootFiber
: 所在组件树根节点, 多次调用ReactDOM.render
渲染不同的组件树,他们会拥有不同的rootFiber
-
-
fiberRoot
的current
会指向当前页面上已渲染内容对应Fiber
树,即current Fiber
树, 由于是首屏渲染,页面中还没有挂载任何DOM,所以fiberRootNode.current
指向的rootFiber
没有任何子Fiber
节点(即current Fiber树为空)。export function createFiberRoot(
containerInfo: any,
tag: RootTag,
hydrate: boolean,
hydrationCallbacks: null | SuspenseHydrationCallbacks,
): FiberRoot {
const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
if (enableSuspenseCallback) {
root.hydrationCallbacks = hydrationCallbacks;
}
// Cyclic construction. This cheats the type system right now because
// stateNode is any.
const uninitializedFiber = createHostRootFiber(tag);
root.current = uninitializedFiber;
uninitializedFiber.stateNode = root;
initializeUpdateQueue(uninitializedFiber);
return root;
} -
进入
render
阶段,根据组件返回的JSX
在内存中依次创建Fiber
节点并连接在一起构建Fiber
树,被称为workInProgress Fiber
树。在构建workInProgress Fiber
树时会尝试复用current Fiber
树中已有的Fiber
节点内的属性,在首屏渲染时只有rootFiber
存在对应的current fiber
(即rootFiber.alternate)。如果没有alternate
(初始化的 rootFiber 是没有 alternate ),那么会创建一个fiber
作为workInProgress
。会用alternate
将新创建的workInProgress
与current
树建立起关联。这个关联过程只有初始化第一次创建alternate
时候进行currentFiber.alternate = workInProgressFiber
workInProgressFiber.alternate = currentFiber -
已构建完的
workInProgress Fiber
树在commit
阶段渲染到页面。fiberRoot
的current
指针指向workInProgress Fiber
树使其变为current Fiber
树
-
-
update 阶段
-
开启一次新的
render
阶段并构建一棵新的workInProgress Fiber
树,和mount
时一样,workInProgress fiber
的创建可以复用current Fiber
树对应的节点数据 -
workInProgress Fiber
树在render
阶段完成构建后进入commit
阶段渲染到页面上。渲染完毕后,workInProgress Fiber
树变为current Fiber
树。
-
五、问题
5.1 为什么 React 有 Fiber 架构,但是 Vue 不需要 Fiber 架构
React
因为先天的不足——无法精确更新,所以需要 React Fiber
把组件渲染工作切片;而vue基于数据劫持,更新粒度很小,没有这个压力;
5.2 之前递归遍历虚拟 Dom 树被打断就得从头开始,为什么有了 React Fiber 就能断点恢复呢?
React Fiber
这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;
5.3 Fiber 解决了什么问题? 为什么 Fiber 能够解决卡顿的问题?
在 React 15
以及之前的版本,React
对于虚拟 DOM
是采用递归方式遍历更新的,比如一次更新,就会从应用根部递归更新,递归一旦开始,中途无法中断,随着项目越来越复杂,层级越来越深,导致更新的时间越来越长,给前端交互上的体验就是卡顿。
React 16
为了解决卡顿问题引入了 fiber
,为什么它能解决卡顿,更新 fiber
的过程叫做 Reconciler
(调和器),每一个 fiber
都可以作为一个执行单元来处理,所以每一个 fiber
可以根据自身的过期时间expirationTime
( v17 版本叫做优先级 lane
)来判断是否还有空间时间执行更新,如果没有时间更新,就要把主动权交给浏览器去渲染,做一些动画,重排( reflow ),重绘 repaints 之类的事情,这样就能给用户感觉不是很卡。然后等浏览器空余时间,在通过 scheduler
(调度器),再次恢复执行单元上来,这样就能本质上中断了渲染,提高了用户体验。