写在专栏开头(叠甲)
-
作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。
-
本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。
-
本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。
本一节的内容
本节的内容我们将讲述React中的一个很重要的数据结构——Fiber ,本节先着重说明什么是 Fiber 结构,它的数据结构是什么,以及 React 为什么要在 16.X 版本后引入 Fiber 结构,之后的章节会讲述从 React Element 到 Fiber 树的过程以及 Fiber 树的生成和更新
Fiber 结构
Fiber 是 React 16.x 新增的一个数据结构,它由对应的 React Element 生成,它的作用我们会在后面讲到,我们先来看它的定义。它在源码的这个位置:https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js
export type Fiber = {
tag: WorkTag,
key: null | string,
elementType: any,
type: any,
stateNode: any,
return: Fiber | null,
child: Fiber | null,
sibling: Fiber | null,
index: number,
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
refCleanup: null | (() => void),
pendingProps: any,
memoizedProps: any,
updateQueue: mixed,
memoizedState: any,
dependencies: Dependencies | null,
mode: TypeOfMode,
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
lanes: Lanes,
childLanes: Lanes,
alternate: Fiber | null,
actualDuration?: number,
actualStartTime?: number,
selfBaseDuration?: number,
treeBaseDuration?: number,
_debugSource?: Source | null,
_debugOwner?: Fiber | null,
_debugIsCurrentlyTiming?: boolean,
_debugNeedsRemount?: boolean,
_debugHookTypes?: Array<HookType> | null,
};
它的数据结构实在过于庞大了,我们拆分成及部分来理解它我们只介绍一些重点的属性,其他的属性想要了解的可以查看源码的注释信息或者查阅其他的资料:
首先是和 DOM相关的属性,它和对应的 React Element 息息相关
tag: WorkTag, //节点的类型
key: null | string, //React Element 的 Key
elementType: any, //React Element 的 type ,原生标签,类组件或者函数组件
type: any, //类组件或者函数组件
stateNode: any, //stateNode 用于记录当前 fiber 所对应的真实 dom 节点或者当前虚拟组件的实例,用于实现对于对DOM的追踪
之后是和整个结构相关的属性,在 React 的 Fiber 架构中,所有的 Fiber 元素将被串联在一起形成一颗双向的链表树,由以下的几个属性来实现
return: Fiber | null, //表述一个元素的父亲
child: Fiber | null, //表述一个元素的第一个孩子
sibling: Fiber | null, //表述一个元素的兄弟
index: number, //在兄弟节点中的位置
可以用一张图形象的概括它:
之后是用于计算 state 和 props 的部分,这是一个 React 组件很重要的组成部分,不过这篇教程主要以讲解 Fiber 结构为主要目的,所以这部分我们会在后续进行讲解,这里按下不表:
pendingProps: any, // 本次渲染需要使用的 props
memoizedProps: any, // 缓存的上次渲染使用的 props
updateQueue: mixed, // 用于状态更新、回调函数、DOM更新的队列
memoizedState: any, /// 缓存的上次渲染后的 state
dependencies: Dependencies | null, // contexts、events 等依赖
之后是副作用相关的部分,也就是说,当我们修改了节点的一些属性,比如 state 或者 props 等的时候,我们的 DOM 可能会发生变化,同时这种变化可能还会影响到孩子节点,具体的流程将会在对 React diff 算法的讲解的时候再次深入,这里我们只是先介绍一下相关的逻辑:
在 render 阶段时,react 会采用深度优先遍历,对 fiber 树进行遍历,Fiber 中会用 flags 表示对当前元素的处理,比如是更新或者删除等等,具体可以查看源码的 Flags 枚举,同时 subtreeFlags 用于表示对孩子节点的处理,而 deletions 则表示我们需要删除的子 Fiber 的序列:
flags: Flags, //对当前节点的处理
subtreeFlags: Flags, //对孩子节点的处理
deletions: Array<Fiber> | null, //要删除的子节点列表
同时,这个阶段会把每一个有副作用的 fiber 筛选出来,放在一个链表中,以下的三个属性就是来标识这个链表的,这个链表将会在之后的阶段用于更新我们的 DOM
nextEffect: Fiber | null, //副作用的下一个 Fiber
firstEffect: Fiber | null, //第一个有副作用的 Fiber
lastEffect: Fiber | null, //最后一个有副作用的 Fiber
lanes 是用于表示执行 fiber 任务的优先级的,这个将会在后续的文章中详细的讲解
alternate 是用于双缓冲树这个结构,简单来说就是:
- react 根据双缓冲机制维护了两个fiber树,一颗用于渲染页面 (current),一颗是 workInProgress Fiber 树,用于在内存中构建,然后方便在构建完成后直接昔换 current Fiber树
- workInprogress Fiber 树的 alternate 指向 Current Fiber树的对应节点, current 表示页面正在使用的 fiber 树
- 当 workInprogress Fiber 树构建完成,workInProgress Fiber 则成为了 current 渲染到页面上,而之前的 current 则缓存起来成为下一次的workInProgress Fiber,完成双缓冲模型
双缓存模型的优势就是提升效率,可以防止只用一颗树更新状态的丢失的情况,又加快了 dom 节点的替换与更新,后续我们还会详细聊聊这个结构。
为什么要使用 Fiber
在 React 15.x 版本以及之前的版本,Reconciliation 算法采用了栈调和器,它最大的缺点是:当我们开始一个任务的时候,一切都是同步进行的,一旦开始执行就不会中断,直到所有的工作流程全部结束为止。当一个页面比较复杂的时候,状态变更时,组件树层层递归开始更新,js 主线程就不得不停止其他工作开始进行渲染的逻辑,用户的点击输入等交互事件、页面动画等都不会得到响应,体验就会非常的差。下面是官方的一副漫画来讲解这个过程:函数堆栈的调用就像下图一样,层级很深
而从刚刚的数据结构中,我们看到,React 将每个节点都封装到了一个 Fiber 中,整个 DOM 树的渲染任务被分成了一个一个小片。当需要进行渲染的时候,React 会从根节点开始一个一个去更新每一个 Fiber ,每当处理完一个 Fiber ,在处理下一个 Fiber 之前,js 可以转而去处理优先级更高、更需要快速响应的任务,而这个优先级被放置在 lanes 这一位中。
React 可以通过优先级来判定是不是中断 Fiber 的处理,调度所有任务的执行,在这样的架构下,虽然任务总的处理时间不变,但是一些需要快速响应的操作可以得到抢占式的响应,类似于操作系统中对线程的抢占式调度,非常强大。对于用户来说,就不会出现因为页面非常复杂,导致渲染任务耗时很长而一致得不到响应的卡顿感受了,官方把它用如下的漫画来表述:
波谷表示执行分片任务,波峰表示执行其他高优先级任务,分片任务在执行结束后释放 js 主线程,高优先级任务可以抢占它,然后继续执行分片任务
拓展:为什么 React 使用链表树这种结构解决了问题
到这里可能有不少人会产生疑问,我理解了 React 的 Fiber 的数据结构了,但是很多人可能还是不清楚,为什么它要采取这样的数据结构呢,它是怎么解决 React 15 以及以前的问题的呢?这里大概描述一下 React 作者和官方给出的说法:
在 React 15 之前,React 要更新一个节点树的时候,他采用深度优先搜索的方法来处理,React 需要迭代整一颗树,对每一个组件执行某些work。对于每一个节点,React 先获取它的子元素列表,然后依次对它的每个孩子进行处理,对孩子的处理亦然,具体可用参考 DFS 算法
这个做法的问题是,它的调用是一个栈,每个还没执行完毕的节点,都会放在运行栈中,知道它的孩子全部处理,依次出栈,它才会被抛出栈。我们不能暂停一个特定组件的执行,然后稍后再恢复执行它。递归模式下,React 只能一直迭代下去,直到所有的组件都被处理一遍,调用清空了才停下来。
而在 React 16 退出的 Fiber 架构中,我们可以用一个指针指向我们当前调用的Fiber,之后当这个 Fiber 执行结束的时候,我们可以让这个指针指向下一个我们要处理的 Fiber ,因为我们的 Fiber 架构中,有指向孩子和兄弟的指针,所以我们可以很容易的找到我们需要操作的下一个元素,先判定它有没有孩子,如果孩子指针是空的则寻找它的下一个兄弟,因为我们的 Fiber 中也有指向它父亲的指针,所以如果这个元素操作完毕后没有需要操作的下一个,它也可以非常方便的返回它的父亲节点。
Fiber 架构做到的是,我们的调用栈并不会增长,它永远只会指向当前在操作的元素,而如果我们暂停我们的操作,在回到渲染过程的时候,我们也可以根据当前的元素快速找到下一个元素,并不依赖于之前的调用栈。
我想这个解释搭配上面的两幅漫画,如果可以帮助你更好的理解,Fiber 架构为什么可以分片,以及它为什么要采用链表树这一核心问题。如果还有疑问的地方也欢迎留言或者私信作者。
总结
这节主要讲述了 React 另一个很重要的数据结构 Fiber 结构,我们现在知道了,一段 Jsx 在 React 里会经过这样的流程:首先被解析成 React Element,再从 React Element 被封装为一个 Fiber ,Fiber 由节点之间的层级关系生成一棵 Fiber 的链表树。
而使用 Fiber 的最重要的原因是,通过 Fiber 可以把一个任务分片,因为 js 的单线程的,如果不分片,任务将会长时间占用 js 主线程,导致用户的请求长时间得不到响应,体验极差。通过 Fiber,高优先级的任务可以在一个分片执行完后抢占 js 主线程,从而提升用户的体验。
那么通过 Fiber 的数据结构,我们得出了一些新的问题:
- React 的任务是怎么样调度的 —— 这里会用到我们提到过的 lanes
- Fiber 树是怎么样生成和更新的 —— 这里会用到我们提到过的双缓冲树和 alternate
- React Element 到 Fiber 的过程发生了什么 —— 这里会处理我们提到过的 state 和 props 的部分
那么在之后的章节里,我们将依次来解决这些问题