Fiber 是什么?
最简单直观的理解是:fiber 是 React 中的虚拟 DOM。但这样的理解不太全面,也不太恰当。
🚩 作为一个JS对象,一个 fiber 节点对应一个 React Element,保存了组件类型、props、对应DOM节点等信息,与视图每个节点一一对应,这就是虚拟DOM。但是在 React 中虚拟 DOM 一词不太恰当,React 支持跨平台,DOM只是其中之一。因此在 React 中还是直接成为 Fiber。
🚩 作为架构来说,Fiber Reconciler, 是 React16 基于 Fiber 实现的 Reconciler,是为了解决 React15 Stack Reconciler 中固有的问题,基于 Fiber Reconciler,React 可以实现:
- 将任务切片处理,支持中断和恢复
- 调整优先级
- 重置并复用任务
- 如果不需要,可以中止工作
🚩 作为时间分片的最小工作单元,每个Fiber节点保存了本次更新中该组件改变的状态 props 和要执行的工作,新增、更新或删除等
Fiber 的结构
Fiber 在 React 中是一个 FiberNode 的类,表示一个 Fiber 节点,以下分三个层面来了解一个 FiberNode 都有哪些属性
与DOM节点相关的属性
作为一个虚拟DOM节点,一个 FiberNode 通过以下属性来保存元素的相关信息:
- tag: 对应组件的类型
- type:对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
- key:key 属性
- elementType
- stateNode: Fiber对应的真实DOM节点
与 Fiber 架构相关的属性
Fiber 是一个链表结构,通过以下三个属性来串联子结点、兄弟节点、父结点。在render阶段遍历组件树时,通过以下三个属性既能根据节点之间的父子兄弟关系串联整棵树,又能实现遍历的中断与恢复。
- child:指向第一个子 Fiber 节点
- sibling:指向第一个兄弟 Fiber 节点
- return:指向父级 Fiber 节点
一个组件实例最多有两个与之对应的 Fiber 节点,一个是当前已渲染的DOM节点对应的 Fiber 节点(mount阶段没有)二是正在进行中的 workInProgress Fiber。这两个 fiber 节点互相通过 alternate 指向对方。
- alternate
优先级相关
- lanes
- childLanes
与更新过程状态相关的属性
每个 Fiber 节点的更新过程,作为一个独立的任务,每次浏览器空闲时更新一个 fiber 节点,然后将控制权交回给浏览器,如果有时间,才会继续更新下一个 fiber 节点,以确保不会长时间占用线程。
由于遍历树的工作是断续进行的,就需要将状态保存在节点中,以下字段保存了本次更新的状态变更
- pendingProps
- memoizedProps
- updateQueue
- memoizedState
- dependencies
- mode
保存了本次更新DOM操作的相关信息
- flags
- subtreeFlags
- deletions
Fiber 架构的工作原理
前置知识
双缓存Fiber树
前面说到,一个组件实例最多有两个与之对应的 Fiber 节点,对应整个应用,就是有两棵Fiber树,每个节点之间通过 alternate 互相关联。
React15中 Reconciler 和 Renderer 是交替工作的,整个过程是同步的,React16 使用了双缓存Fiber树,workInProgress 树在内存中构建完成后才一次性 commit,这个过程是异步可中断的。
mount 阶段 fiber 节点没有与之对应的 alternate,因此可以通过 alternate 是否等于 null 来判断当前是 mount 阶段还是 update 阶段。
在 update 阶段,通过对比新旧 Fiber 节点决定是否可以复用旧的节点。这个对比的过程就是 Diff 算法。等到构建出一棵完整的树,再一次性提交给浏览器进行渲染,然后替换掉 current 树成为新的 current 树。
fiberRoot 与 rootFiber
fiberRoot 是整个应用的根节点,它是始终唯一的,它有一个 current 属性,指向 rootFiber,表示 current Fiber 树
rootFiber 是 <App /> 所在 fiber 树的根节点,它的 child 指向 App 对应的 fiber 节点。每次更新调用 ReactDOM.render 都会创建新的 rootFiber,指向 workInProgress 树。
工作过程
以下面这段代码为例:
function App() {
const [num, add] = useState(0);
return <div onClick={() => add(num + 1}>{num}</div>
}
ReactDOM.render(<App />, document.getElementById('app'));
mount 阶段
首次执行 ReactDOM.render 会创建 fiberRoot 和 rootFiber,fiberRoot.current 指向 rootFiber,此时 rootFiber 还没有子节点。
接下来进入 render 阶段,会创建一个新的 rootFiber,然后依次遍历子组件,创建fiber节点,最终构建出一棵 fiber 树
这是异步更新的过程,从 rootFiber 开始向下深度优先遍历,为遍历到的 fiber 节点调用 beginWork 方法创建子 fiber 节点,并将这两个 fiber 节点连接起来。在update阶段时,如果节点可以复用或者优先级不够,则会直接复用旧的子fiber节点。
当遍历到叶子节点,就会调用 completeWork 处理 Fiber 节点,根据
fiber.tag调用不同的处理逻辑,这个过程同样分mount/update阶段,主要处理 props、注册回调函数、判断是否需要生成 DOM 节点,update阶段会设置fiber.effectTag并保存到effectList链表中,mount阶段则是生成新的DOM节点,然后将子级DOM树插入到当前生成的DOM节点,最终构建出一棵 WIP 树的同时,就已经完成了DOM树的构建,直接将根节点插入到页面即可。当某个 fiber 节点执行完
completeWork,如果其存在兄弟 Fiber 节点,即fiber.sibling !== null,就会为其兄弟 fiber 节点调用beginWork,重复上面的步骤。如果不存在兄弟节点就为父级 fiber 节点调用completeWork。最终回到rootFiber,就完成了 workInProgress 树的构建。在以上过程中,每次调用
beginWork/completeWork处理完一个 fiber 节点,将返回的节点赋值给 workInProgress,然后将控制权交回浏览器,浏览器空闲时继续处理,否则中断任务,等待下次空闲再恢复。这种将整个树的更新拆分为一个个节点进行处理的过程,被称为时间切片。
构建完成的 workInProgress Fiber 树则在 commit 阶段 渲染到页面,然后会将 fiberRoot.current 指向 workInProgress 树,作为下次更新用于比较的 current 树。
update 阶段
当用户交互触发状态改变,会开启新一轮的 render 阶段并构建出一棵新的 workInProgress 树,在构建的过程中,每个节点通过 alternate 与 current Fiber 树的每个节点互相关联并对比,决定是否复用 current fiber 节点。
同样地,workInProgress 树构建完成后进入 commit 阶段,然后更新 fiberRoot.current 指向 workInProgress 树。