Fiber
既是 react 定义的一种数据结构,也是我们常说的虚拟 DOM
由一个个Fiber
节点,构成的树就是Fiber Tree
为什么要设计 Fiber 数据结构
React15 在 render 阶段的 reconcile 是不可打断的,这会在进行大量节点的 reconcile 时可能产生卡顿,因为浏览器所有的时间都交给了 js 执行,而JS线程
与GUI渲染线程
互斥。
为此 React16 之后就有了 scheduler 进行时间片的调度,给每个 task(工作单元)一定的时间,如果在这个时间内没执行完,也要交出执行权给浏览器进行绘制和重排。
而异步可中断的更新需要一定的数据结构,在内存中来保存工作单元的信息,所以出现了 Fiber 数据结构。
Fiber 的作用
- 工作单元,任务分解 :Fiber 最重要的功能就是作为工作单元,保存原生节点或者组件节点对应信息(包括优先级)。这些节点通过指针的形式,构成 Fiber 树
- 增量渲染:通过 jsx 对象和 current Fiber 的对比,生成最小的差异补丁
(Flags)
,应用到真实节点上 - 根据优先级暂停、继续、排列优先级:Fiber 节点上保存了优先级,能通过不同节点优先级的对比,达到任务的暂停、继续、排列优先级等能力,也为上层实现批量更新、Suspense 提供了基础
- 保存状态:因为 Fiber 能保存状态和更新的信息,所以就能实现函数组件的状态更新,也就是 hooks
Fiber 长什么样
Fiber 节点的构建函数如下:
//ReactFiber.old.js
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
//作为静态的数据结构 保存节点的信息
this.tag = tag; //对应组件的类型
this.key = key; //key 属性
this.elementType = null;//元素类型
this.type = null; //func 或者 class
this.stateNode = null; //真实 dom 节点
//作为 fiber 树架构 连接成 fiber 树
this.return = null; //指向父节点
this.child = null; //指向child
this.sibling = null; //指向兄弟节点
this.index = 0;
this.ref = null;
//用作为工作单元 来计算state
this.pendingProps = pendingProps;
this.memoizedProps = null;
this.updateQueue = null;
this.memoizedState = null;
this.dependencies = null;
this.mode = mode;
//effect相关
this.effectTag = NoEffect;
this.nextEffect = null;
this.firstEffect = null;
this.lastEffect = null;
//优先级相关的属性
this.lanes = NoLanes;
this.childLanes = NoLanes;
//current和workInProgress的指针
this.alternate = null;
}
举个例子
function App() {
return (
<div>
i am
<span>KaSong</span>
</div>
)
}
Fiber 之双缓存
什么是双缓存
当我们用 canvas 绘制动画,每一帧绘制前都会调用 ctx.clearRect 清除上一帧的画面。
如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。
为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。
这种在内存中构建并直接替换的技术叫做双缓存。
React 使用“双缓存”来完成 Fiber 树的构建与替换——对应着DOM树的创建与更新。
双缓存 Fiber 树
在 React 中最多会同时存在两棵 Fiber 树
- 在屏幕上显示的 DOM 元素对应的 Fiber 树称为 current Fiber 树
- current Fiber 树 中的 Fiber 节点 被称为
current fiber
- current Fiber 树 中的 Fiber 节点 被称为
- 正在内存中构建的 Fiber 树称为 workInProgress Fiber 树
- workInProgress Fiber 树 中的 Fiber 节点 被称为
workInProgress fiber
- workInProgress Fiber 树 中的 Fiber 节点 被称为
两棵树的 Fiber 节点,通过alternate 属性
连接
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
workInProgress Fiber 树 构建完成交给 Renderer 渲染在页面上后,把根节点(fiberRootNode)
的 current
指针指向 workInProgress Fiber 树,此时 workInProgress Fiber树 就变为 current Fiber 树。
每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成 DOM 更新。
接下来我们以具体例子讲解 mount 时、update 时的构建/替换流程。
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/>
所在组件树的根节点。由于是首屏渲染,页面中还没有挂载任何 DOM,所以
fiberRootNode.current
指向的rootFiber
没有任何子Fiber
节点(即 current Fiber 树为空)
- 其中
- 接下来进入 render 阶段,根据组件返回的 JSX 在内存中依次创建 Fiber节点 并连接在一起构建 Fiber树,被称为 workInProgress Fiber 树。(下图中右侧为内存中构建的树,左侧为页面显示的树)
在构建 workInProgress Fiber树 时会尝试复用 current Fiber树 中已有的 Fiber 节点 内的属性。 在首屏渲染时只有
rootFiber
存在对应的current fiber
(即rootFiber.alternate
)
- 图中右侧已构建完的 workInProgress Fiber 树 在 commit 阶段 渲染到页面。
此时 DOM 更新为右侧树对应的样子。
fiberRootNode
的current
指针指向 workInProgress Fiber树 使其变为 current Fiber 树。
Update 时
- 接下来我们点击 p 节点触发状态改变,这会开启一次新的 render 阶段并构建一棵新的 workInProgress Fiber 树。
和 mount 时一样,workInProgress fiber
的创建可以复用 current Fiber 树 对应的节点数据。
这个决定是否复用的过程就是 Diff 算法
- workInProgress Fiber 树 在 render 阶段完成构建后,进入 commit 阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树 变为 current Fiber 树。