认识
一、认识
React Fiber
解决了在 React 16
之前, 渲染任务都是同步的, 不可中断。这会在进行大量节点的reconcile
时可能产生卡顿,因为浏览器所有的时间都交给了JS
执行,并且JS
的执行是单线程。React16
之后就有了scheduler
进行时间片的调度,给每个任务(工作单元)一定的时间,如果在这个时间内没有执行完,也要交出执行权给浏览器进行绘制和重排。所以异步可中断的更新需要一定的数据结构在内存中保存工作单元的信息。通过 Fiber
架构, React
得以实现了 将不可中断的同步更新改为可中断的异步更新。
Fiber
作为静态数据结构, 每个 Fiber
节点对应一个 ReactElement
,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM
节点等信息。Fiber
作为动态工作单元, 每个 Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue
存储了更新队列, flags
存储了要执行的操作, lang
表示当前更新优先级, childLanes
表示当前 Fiber
需要更新。 Fiber
通过child
、sibling
、return
(指向父节点)来形成Fiber
树。为什么父级指针叫做return
而不是parent
或者father
呢?, 因为作为一个工作单元, return
指节点执行完 completeWork
后会返回的下一个节点, 子Fiber
节点及其兄弟节点完成工作后会返回其父级节点, 所以用return
指代父级节点。
Fiber
工作: JSX
会编译为 ReactElement
, 在 Render BeginWork
递阶段, 从根节点 Fiber
开始, 从上而下根据 ReactElement
生成新的 Fiber
节点。如果有旧 Fiber
, 会基于 Reconcile Diff
比较 ReactElement
和 旧 Fiber
, 对比差异是否生成新的 Fiber
。此时, 内存中有两颗 Fib er
树, Fiber
双缓冲树 在经过 Reconcile(diff)
形成了新的 workInProgress Fiber
然后将 workInProgress Fiber
切换成 Current Fiber
应用到 真实 DOM
中。 存在 双 Fiber
的好处是在内存中形成视图的描述,在最后应用到 DOM
中,减少了对dom
的操作。
二、结构
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;
}
}
2.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
指代父级节点。
2.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 返回的组件
2.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;
}
2.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
的操作。
3.1 Fiber两棵树
在React
中最多会从同时存在两颗Fiber
树。当前屏幕上显示(呈现)的内容对应的Fiber
树称为current Fiber
树,正在内存中构建的Fiber
树称为workInProgress Fiber
树。
3.2 Fiber树连接
current Fiber
树中的Fiber
节点被称为current fiber
,workInProgress Fiber
树中的Fiber
节点被称为workInProgress fiber
,他们通过alternate
属性连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
3.3 Fiber树切换
React
应用的根节点通过使current
指针在不同Fiber
树的rootFiber间切换来完成
current Fiber树指向的切换。即当
workInProgress Fiber树构建完成交给
Renderer渲染在页面上后,应用根节点的
current指针指向
workInProgress Fiber树,此时
workInProgress Fiber树就变为
current Fiber`树。
3.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
树。
-
四、问题
4.1 React Fiber 是如何实现更新过程可控?
React 15
及其之前的版本: React
对于虚拟 DOM
是采用同步递归方式遍历更新的, 比如一次更新, 就会从应用根部递归更新, 递归一旦开始, 中途无法中断, 随着项目越来越复杂,层级越来越深,导致更新的时间越来越长,给前端交互上的体验就是卡顿。
React 16
之后: 为了解决卡顿问题引入了 Fiber
, Fiber
作为动态的工作单元来说, 每个 Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue
存储了更新队列, flags
存储了要执行的操作, lang
表示当前更新优先级, childLanes
表示当前 Fiber
需要更新。而且, Fiber
节点以链表形式组织, 支持快速暂停和恢复任务执行状态, 确保高优任务(如用户交互)及时响应, 低优任务(如非紧急更新)可延迟执行。结合 Fiber
架构和时间切片, 将原本同步、不可中断的更新过程拆分成可中断的小任务, 根据优先级灵活调度任务,保证用户体验流畅。通过 Scheduler
调度器使用 MessageChannel
、 setTimeout
控制每个任务的执行时长, Scheduler
能够随时检查是否有高优先级任务插入, 如果存在,就中断当前任务,转而处理更紧急的任务。从而允许高优先级任务(如用户交互)打断低优先级任务, 有助于避免长时间的同步渲染阻塞主线程,提升应用响应性。
Scheduler
调度器 将更新任务以对应的优先级加入任务队列, 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTime
与 currentTime
, 如果 startTime > currentTime
, 表示当前任务未就绪, 存入 timerQueue
。并根据开始时间重新排列 timerQueue
中任务的顺序。当 timerQueue
中有任务就绪,即startTime <= currentTime
,我们将其取出并加入taskQueue
。 timerQueue
和 taskQueue
两个队列为了能在 O(1)
的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程, 时间最小的, 也就是最早的任务。 Scheduler
调度更新任务时的表现为: 1. 优先级高的任务会打断低优先级任务的执行, 每次执行的任务是所有任务中优先级最高的; 2. 如果两个任务的优先级相同, 不会开启新的调度; 3. 低优先级的任务存在过期时间, 等到失效后, 以同步任务高优执行。防止低优先级任务一直得不到执行, 导致饥饿问题的出现
React Fiber
在更新过程中, 整体渲染被拆分为两个阶段, Render
阶段用于生成一棵新的 Fiber
树,并为后续的 DOM
更新准备好 副作用列表(effect list
), Render
阶段的任务可随时中断, 交给 Scheduler
控制, 这样能确保 React
应用流畅响应用户交互; Commit
阶段是 React
更新流程中唯一会直接操作 DOM
的阶段,它根据 Render
阶段生成的副作用列表执行实际的更新操作。Commit
阶段同步执行, 批量更新 DOM
, 保证用户体验一致性和页面稳定。简单来说,由于 Render
阶段的操作对用户来说其实是不可见的,所以就算打断再重启,对用户来说也是零感知。而 commit
阶段的操作则涉及真实 DOM
的渲染,所以这个过程必须用同步渲染来求稳。
4.2 Fiber 为什么是 React 性能的一个飞跃?
React 15
及其之前的版本: React
对于虚拟 DOM
是采用同步递归方式遍历更新的, 比如一次更新, 就会从应用根部递归更新, 递归一旦开始, 中途无法中断, 随着项目越来越复杂,层级越来越深,导致更新的时间越来越长,给前端交互上的体验就是卡顿。
React 16
之后: 为了解决卡顿问题引入了 Fiber
, Fiber
作为动态的工作单元来说, 每个 Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue
存储了更新队列, flags
存储了要执行的操作, lang
表示当前更新优先级, childLanes
表示当前 Fiber
需要更新。而且, Fiber
节点以链表形式组织, 支持快速暂停和恢复任务执行状态, 确保高优任务(如用户交互)及时响应, 低优任务(如非紧急更新)可延迟执行。结合 Fiber
架构和时间切片, 将原本同步、不可中断的更新过程拆分成可中断的小任务, 根据优先级灵活调度任务,保证用户体验流畅。通过 Scheduler
调度器使用 MessageChannel
、 setTimeout
控制每个任务的执行时长, Scheduler
能够随时检查是否有高优先级任务插入, 如果存在,就中断当前任务,转而处理更紧急的任务。从而允许高优先级任务(如用户交互)打断低优先级任务, 有助于避免长时间的同步渲染阻塞主线程,提升应用响应性。
Scheduler
调度器 将更新任务以对应的优先级加入任务队列, 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTime
与 currentTime
, 如果 startTime > currentTime
, 表示当前任务未就绪, 存入 timerQueue
。并根据开始时间重新排列 timerQueue
中任务的顺序。当 timerQueue
中有任务就绪,即startTime <= currentTime
,我们将其取出并加入taskQueue
。 timerQueue
和 taskQueue
两个队列为了能在 O(1)
的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程, 时间最小的, 也就是最早的任务。 Scheduler
调度更新任务时的表现为: 1. 优先级高的任务会打断低优先级任务的执行, 每次执行的任务是所有任务中优先级最高的; 2. 如果两个任务的优先级相同, 不会开启新的调度; 3. 低优先级的任务存在过期时间, 等到失效后, 以同步任务高优执行。防止低优先级任务一直得不到执行, 导致饥饿问题的出现
React Fiber
在更新过程中, 整体渲染被拆分为两个阶段, Render
阶段用于生成一棵新的 Fiber
树,并为后续的 DOM
更新准备好 副作用列表(effect list
), Render
阶段的任务可随时中断, 交给 Scheduler
控制, 这样能确保 React
应用流畅响应用户交互; Commit
阶段是 React
更新流程中唯一会直接操作 DOM
的阶段,它根据 Render
阶段生成的副作用列表执行实际的更新操作。Commit
阶段同步执行, 批量更新 DOM
, 保证用户体验一致性和页面稳定。简单来说,由于 Render
阶段的操作对用户来说其实是不可见的,所以就算打断再重启,对用户来说也是零感知。而 commit
阶段的操作则涉及真实 DOM
的渲染,所以这个过程必须用同步渲染来求稳。
4.3 为什么 React 有 Fiber 架构, 但是 Vue 不需要 Fiber 架构
React
因为先天的不足——无法精确更新,所以需要 React Fiber
把组件渲染工作切片;而vue基于数据劫持,更新粒度很小,没有这个压力;
React 15
及其之前的版本: React
对于虚拟 DOM
是采用同步递归方式遍历更新的, 比如一次更新, 就会从应用根部递归更新, 递归一旦开始, 中途无法中断, 随着项目越来越复杂,层级越来越深,导致更新的时间越来越长,给前端交互上的体验就是卡顿。
React 16
之后: 为了解决卡顿问题引入了 Fiber
, Fiber
作为动态的工作单元来说, 每个 Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue
存储了更新队列, flags
存储了要执行的操作, lang
表示当前更新优先级, childLanes
表示当前 Fiber
需要更新。而且, Fiber
节点以链表形式组织, 支持快速暂停和恢复任务执行状态, 确保高优任务(如用户交互)及时响应, 低优任务(如非紧急更新)可延迟执行。结合 Fiber
架构和时间切片, 将原本同步、不可中断的更新过程拆分成可中断的小任务, 根据优先级灵活调度任务,保证用户体验流畅。通过 Scheduler
调度器使用 MessageChannel
、 setTimeout
控制每个任务的执行时长, Scheduler
能够随时检查是否有高优先级任务插入, 如果存在,就中断当前任务,转而处理更紧急的任务。从而允许高优先级任务(如用户交互)打断低优先级任务, 有助于避免长时间的同步渲染阻塞主线程,提升应用响应性。
Scheduler
调度器 将更新任务以对应的优先级加入任务队列, 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTime
与 currentTime
, 如果 startTime > currentTime
, 表示当前任务未就绪, 存入 timerQueue
。并根据开始时间重新排列 timerQueue
中任务的顺序。当 timerQueue
中有任务就绪,即startTime <= currentTime
,我们将其取出并加入taskQueue
。 timerQueue
和 taskQueue
两个队列为了能在 O(1)
的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程, 时间最小的, 也就是最早的任务。 Scheduler
调度更新任务时的表现为: 1. 优先级高的任务会打断低优先级任务的执行, 每次执行的任务是所有任务中优先级最高的; 2. 如果两个任务的优先级相同, 不会开启新的调度; 3. 低优先级的任务存在过期时间, 等到失效后, 以同步任务高优执行。防止低优先级任务一直得不到执行, 导致饥饿问题的出现
因此, 由于 React
历史渲染的设计思想, 通过引入 Fiber
架构, 来支持异步、可中断的任务调度, 保证关键任务优先执行, 使得 UI
更新更平滑。
Vue
从设计之初开始, 就采用响应式依赖追踪机制, 基于 Object.defineProperty
或者 Proxy
来监听数据变化, 当数据变化时, Vue
能精确地知道哪些组件受到了影响, 从而只对必要的组件进行更新, 这种精细的依赖收集机制使得更新过程本身就较为高效。Vue
在更新过程中会将多次数据变更批量收集,然后在下一个事件循环中统一执行更新(通常借助 nextTick
实现异步刷新)。这种方式能有效避免频繁的重复渲染,也确保了界面的稳定性。
因此, 由于 Vue
的响应式系统天然能够精确定位需要更新的部分,加上批量更新策略,在大多数常规应用场景下,其同步更新的模式已能满足性能需求,无需像 React
那样引入复杂的时间切片和任务调度机制。
4.4 之前递归遍历虚拟 Dom 树被打断就得从头开始,为什么有了 React Fiber 就能断点恢复呢?
React Fiber
这种数据结构使得节点可以回溯到其父节点,只要保留下中断的节点索引,就可以恢复之前的工作进度;
4.5 Fiber 解决了什么问题? 为什么 Fiber 能够解决卡顿的问题?
React 15
及其之前的版本: React
对于虚拟 DOM
是采用同步递归方式遍历更新的, 比如一次更新, 就会从应用根部递归更新, 递归一旦开始, 中途无法中断, 随着项目越来越复杂,层级越来越深,导致更新的时间越来越长,给前端交互上的体验就是卡顿。
React 16
之后: 为了解决卡顿问题引入了 Fiber
, Fiber
作为动态的工作单元来说, 每个 Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue
存储了更新队列, flags
存储了要执行的操作, lang
表示当前更新优先级, childLanes
表示当前 Fiber
需要更新。而且, Fiber
节点以链表形式组织, 支持快速暂停和恢复任务执行状态, 确保高优任务(如用户交互)及时响应, 低优任务(如非紧急更新)可延迟执行。结合 Fiber
架构和时间切片, 将原本同步、不可中断的更新过程拆分成可中断的小任务, 根据优先级灵活调度任务,保证用户体验流畅。通过 Scheduler
调度器使用 MessageChannel
、 setTimeout
控制每个任务的执行时长, Scheduler
能够随时检查是否有高优先级任务插入, 如果存在,就中断当前任务,转而处理更紧急的任务。从而允许高优先级任务(如用户交互)打断低优先级任务, 有助于避免长时间的同步渲染阻塞主线程,提升应用响应性。
Scheduler
调度器 将更新任务以对应的优先级加入任务队列, 在函数中, 不同的优先级意味着不同时长的任务过期时间。同时会比较 startTime
与 currentTime
, 如果 startTime > currentTime
, 表示当前任务未就绪, 存入 timerQueue
。并根据开始时间重新排列 timerQueue
中任务的顺序。当 timerQueue
中有任务就绪,即startTime <= currentTime
,我们将其取出并加入taskQueue
。 timerQueue
和 taskQueue
两个队列为了能在 O(1)
的时间复杂度里找到两个队列中时间最早的那个任务, 采用的是最小堆的数据结构, 每次存入任务的过程就是最小堆自动调整的过程, 时间最小的, 也就是最早的任务。 Scheduler
调度更新任务时的表现为: 1. 优先级高的任务会打断低优先级任务的执行, 每次执行的任务是所有任务中优先级最高的; 2. 如果两个任务的优先级相同, 不会开启新的调度; 3. 低优先级的任务存在过期时间, 等到失效后, 以同步任务高优执行。防止低优先级任务一直得不到执行, 导致饥饿问题的出现
React Fiber
在更新过程中, 整体渲染被拆分为两个阶段, Render
阶段用于生成一棵新的 Fiber
树,并为后续的 DOM
更新准备好 副作用列表(effect list
), Render
阶段的任务可随时中断, 交给 Scheduler
控制, 这样能确保 React
应用流畅响应用户交互; Commit
阶段是 React
更新流程中唯一会直接操作 DOM
的阶段,它根据 Render
阶段生成的副作用列表执行实际的更新操作。Commit
阶段同步执行, 批量更新 DOM
, 保证用户体验一致性和页面稳定。简单来说,由于 Render
阶段的操作对用户来说其实是不可见的,所以就算打断再重启,对用户来说也是零感知。而 commit
阶段的操作则涉及真实 DOM
的渲染,所以这个过程必须用同步渲染来求稳。
总结来说, React
采用 Fiber
架构主要是为了支持异步、可中断的任务调度,从而提升在复杂场景下的用户体验
4.6 说说对 Fiber 架构的理解?解决了什么问题?
React Fiber
解决了在 React 16
之前, 渲染任务都是同步的, 不可中断。这会在进行大量节点的reconcile
时可能产生卡顿,因为浏览器所有的时间都交给了JS
执行,并且JS
的执行是单线程。React16
之后就有了scheduler
进行时间片的调度,给每个任务(工作单元)一定的时间,如果在这个时间内没有执行完,也要交出执行权给浏览器进行绘制和重排。所以异步可中断的更新需要一定的数据结构在内存中保存工作单元的信息。通过 Fiber
架构, React
得以实现了 将不可中断的同步更新改为可中断的异步更新。
Fiber
作为静态数据结构, 每个 Fiber
节点对应一个 ReactElement
,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM
节点等信息。Fiber
作为动态工作单元, 每个 Fiber
节点保存了本次更新中该组件改变的状态、要执行的工作, updateQueue
存储了更新队列, flags
存储了要执行的操作, lang
表示当前更新优先级, childLanes
表示当前 Fiber
需要更新。 Fiber
通过child
、sibling
、return
(指向父节点)来形成Fiber
树。为什么父级指针叫做return
而不是parent
或者father
呢?, 因为作为一个工作单元, return
指节点执行完 completeWork
后会返回的下一个节点, 子Fiber
节点及其兄弟节点完成工作后会返回其父级节点, 所以用return
指代父级节点。
Fiber
工作: JSX
会编译为 ReactElement
, 在 Render BeginWork
递阶段, 从根节点 Fiber
开始, 从上而下根据 ReactElement
生成新的 Fiber
节点。如果有旧 Fiber
, 会基于 Reconcile Diff
比较 ReactElement
和 旧 Fiber
, 对比差异是否生成新的 Fiber
。此时, 内存中有两颗 Fib er
树, Fiber
双缓冲树 在经过 Reconcile(diff)
形成了新的 workInProgress Fiber
然后将 workInProgress Fiber
切换成 Current Fiber
应用到 真实 DOM
中。 存在 双 Fiber
的好处是在内存中形成视图的描述,在最后应用到 DOM
中,减少了对dom
的操作。
总结来说, React
采用 Fiber
架构主要是为了支持异步、可中断的任务调度,从而提升在复杂场景下的用户体验