我们可以从三个维度来理解 Fiber:
- 是一种架构,称之为 Fiber 架构
- 是一种数据类型
- 动态的工作单元
Fiber 架构
在 React v16之前,使用的是 Stack Reconciler,因此那个时候的 React 架构被称之为 Stack 架构。从 React v16 开始,重构了整个架构,引入了 Fiber,因此新的架构也被称之为 Fiber 架构。Stack Reconciler 也变成了 Fiber Reconciler。各个FiberNode之间通过链表的形式串联起来:
function FiberNode(tag, pendingProps, key, mode) {
// ...
// 周围的 Fiber Node 通过链表的形式进行关联
this.return = null;
this.child = null;
this.sibling = null;
this.index = 0;
// ...
}
数据类型
Fiber 本质上也是一个对象,是在之前 React 元素基础上的一种升级版本。每个 FiberNode 对象里面会包含 React 元素的类型、周围链接的FiberNode以及 DOM 相关信息:
function FiberNode(tag, pendingProps, key, mode) {
// 类型
this.tag = tag;
this.key = key;
this.elementType = null;
this.type = null;
this.stateNode = null; // 映射真实 DOM
// ...
}
动态的工作单元
在每个 FiberNode 中,保存了本次更新中该 React 元素变化的数据,还有就是要执行的工作(增、删、更新)以及副作用的信息:
function FiberNode(tag, pendingProps, key, mode) {
// ...
// 副作用相关
this.flags = NoFlags;
this.subtreeFlags = NoFlags;
this.deletions = null;
// 与调度优先级有关
this.lanes = NoLanes;
this.childLanes = NoLanes;
// ...
}
为什么指向父 FiberNode 的字段叫做 return 而非 parent?
因为作为一个动态的工作单元,return 指代的是 FiberNode 执行完 completeWork 后返回的下一个 FiberNode,这里会有一个返回的动作,因此通过 return 来指代父 FiberNode
Fiber 双缓冲
Fiber 架构中的双缓冲工作原理类似于显卡的工作原理。
显卡分为前缓冲区和后缓冲区。首先,前缓冲区会显示图像,之后,合成的新的图像会被写入到后缓冲区。一旦后缓冲区写入图像完毕,就会前后缓冲区进行一个互换,这种将数据保存在缓冲区再进行互换的技术,就被称之为双缓冲技术。
Fiber 架构同样用到了这个技术,在 Fiber 架构中,同时存在两颗 Fiber Tree,一颗是真实 UI 对应的 Fiber Tree,可以类比为显卡的前缓冲区,另外一颗是在内存中构建的 FiberTree,可以类比为显卡的后缓冲区。
在 React 源码中,很多方法都需要接收两颗 FiberTree:
function cloneChildFibers(current, workInProgress){
// ...
}
current 指的就是前缓冲区的 FiberNode,workInProgress 指的就是后缓冲区的 FiberNode。
两个 FiberNode 会通过 alternate 属性相互指向:
current.alternate = workInProgress;
workInProgress.alternate = current;
接下来我们从首次渲染(mount)和更新(update)这两个阶段来看一下 FiberTree 的形成以及双缓存机制:
mount 阶段
首先最顶层有一个 FiberNode,称之为 FiberRootNode,该 FiberNode 会有一些自己的任务:
- Current Fiber Tree 与 Wip Fiber Tree 之间的切换
- 应用中的过期时间
- 应用的任务调度信息
现在假设有这么一个结构:
<body>
<div id="root"></div>
</body>
function App(){
const [num, add] = useState(0);
return (
<p onClick={() => add(num + 1)}>{num}</p>
);
}
const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(<App />);
当执行 ReactDOM.createRoot 的时候,会创建如下的结构:
此时会有一个 HostRootFiber,FiberRootNode 通过 current 来指向 HostRootFiber。
接下来进入到 mount 流程,该流程会基于每个 React 元素以深度优先的原则依次生成 wip FiberNode,并且每一个 wipFiberNode 会连接起来,如下图所示:
生成的 wip FiberTree 里面的每一个 FiberNode 会和 current FiberTree 里面的 FiberNode进行关联,关联的方式就是通过 alternate。但是目前 currentFiberTree里面只有一个 HostRootFiber,因此就只有这个 HostRootFiber 进行了 alternate 的关联。
当 wip FiberTree生成完毕后,也就意味着 render 阶段完毕了,此时 FiberRootNode就会被传递给 Renderer(渲染器),接下来就是进行渲染工作。渲染工作完毕后,浏览器中就显示了对应的 UI,此时 FiberRootNode.current 就会指向这颗 wip Fiber Tree,曾经的 wip Fiber Tree 它就会变成 current FiberTree,完成了双缓存的工作:
update 阶段
点击 p 元素,会触发更新,这一操作就会开启 update 流程,此时就会生成一颗新的 wip Fiber Tree,流程和之前是一样的
新的 wip Fiber Tree 里面的每一个 FiberNode 和 current Fiber Tree 的每一个 FiberNode 通过 alternate 属性进行关联。
当 wip Fiber Tree 生成完毕后,就会经历和之前一样的流程,FiberRootNode 会被传递给 Renderer 进行渲染,此时宿主环境所渲染出来的真实 UI 对应的就是左边 wip Fiber Tree 所对应的 DOM 结构,FiberRootNode.current 就会指向左边这棵树,右边的树就再次成为了新的 wip Fiber Tree
这个就是 Fiber双缓存的工作原理。
另外值得一提的是,开发者是可以在一个页面创建多个应用的,比如:
ReactDOM.createRoot(rootElement1).render(<App1 />);
ReactDOM.createRoot(rootElement2).render(<App2 />); ReactDOM.createRoot(rootElement3).render(<App3 />);
在上面的代码中,我们创建了 3 个应用,此时就会存在 3 个 FiberRootNode,以及对应最多 6 棵 Fiber Tree 树。
真题解析
题目:谈一谈你对 React 中 Fiber 的理解以及什么是 Fiber 双缓冲?
参考答案:
Fiber 可以从三个方面去理解:
- FiberNode 作为一种架构:在 React v15 以及之前的版本中,Reconceiler 采用的是递归的方式,因此被称之为 Stack Reconciler,到了 React v16 版本之后,引入了 Fiber,Reconceiler 也从 Stack Reconciler 变为了 Fiber Reconceiler,各个 FiberNode 之间通过链表的形式串联了起来。
- FiberNode 作为一种数据类型:Fiber 本质上也是一个对象,是之前虚拟 DOM 对象(React 元素,createElement 的返回值)的一种升级版本,每个 Fiber 对象里面会包含 React 元素的类型,周围链接的 FiberNode,DOM 相关信息。
- FiberNode 作为动态的工作单元:在每个 FiberNode 中,保存了“本次更新中该 React 元素变化的数据、要执行的工作(增、删、改、更新Ref、副作用等)”等信息。
所谓 Fiber 双缓冲树,指的是在内存中构建两颗树,并直接在内存中进行替换的技术。在 React 中使用 Wip Fiber Tree 和 Current Fiber Tree 这两颗树来实现更新的逻辑。Wip Fiber Tree 在内存中完成更新,而 Current Fiber Tree 是最终要渲染的树,两颗树通过 alternate 指针相互指向,这样在下一次渲染的时候,直接复用 Wip Fiber Tree 作为下一次的渲染树,而上一次的渲染树又作为新的 Wip Fiber Tree,这样可以加快 DOM 节点的替换与更新。