02-react Fiber 架构

原文链接
Fiber 是对 React 核心算法的重构,facebook 团队使用两年多的时间去重构 React 的核心算法,在React16 以上的版本中引入了 Fiber 架构;

为什么需要 Fiber

在 React15 及以前,Reconciler 采用递归的方式创建虚拟 DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。
为什么会造成卡顿,我们需要了解浏览器在一帧中都做了什么?

浏览器一帧会干什么

我们知道,在浏览器中,页面是一帧一帧绘制出来的,渲染的帧率与设备的刷新率保持一致。一般情况下,设备的屏幕刷新率为 1s 60 次,当每秒内绘制的帧数(FPS)超过 60 时,页面渲染是流畅的;而当 FPS 小于 60 时,会出现一定程度的卡顿现象。下面来看完整的一帧中,具体做了哪些事情:
在这里插入图片描述

  • 脚本执行(JavaScript):脚本造成了需要重绘的改动,比如增删 DOM、请求动画等
  • 样式计算(CSS Object Model):级联地生成每个节点的生效样式。
  • 布局(Layout):计算布局,执行渲染算法
  • 重绘(Paint):各层分别进行绘制(比如 3D 动画)
  • 合成(Composite):合成各层的渲染结果


JavaScript 引擎和页面渲染引擎是在同一个渲染线程之内,两者是互斥关系。如果在某个阶段执行任务特别长,例如在定时器阶段或 Frame start 阶段执行时间非常长,时间已经明显超过了16ms,那么就会阻塞页面的渲染,从而出现卡顿现象。

React 15 会递归比对 VirtualDOM 树,找出需要变动的节点,然后同步更新它们,这个过程 React 称 reconcilation 也就是之前文章提到的协调器。在 reconcilation 期间,React 会霸占着浏览器资源,一则会导致用户触发的事件得不到响应, 二则会导致掉帧,用户可以感知到这些卡顿。可以通过下面的 demo,体验同步模式以及 CM 模式下 react 渲染的速度:
在这里插入图片描述

为了解决这个问题,React16 将递归的无法中断的更新重构为异步的可中断更新,由于曾经用于递归的虚拟 DOM 数据结构已经无法满足需要于;是,全新的 Fiber 架构应运而生。Fiber 将渲染/更新过程拆分为一个个小块的任务,通过合理的调度机制来调控时间,指定任务执行的时机,从而降低页面卡顿的概率,提升页面交互体验。通过 Fiber 架构,让 reconcilation 过程变得可被中断。适时地让出 CPU 执行权,可以让浏览器及时地响应用户的交互。

Fiber 是什么


Fiber 可以理解为:React 内部实现的一套状态更新机制。支持任务不同优先级,可中断与恢复,并且恢复后可以复用之前的中间状态。其中每个任务更新单元为 React Element 对应的 Fiber 节点。

对于 React 来说, Fiber 包含三层含义:

  1. 架构层面的 Fiber Reconciler;
  2. 一种数据结构;
  3. 动态的工作单元;

1. 架构层面的 Fiber Reconciler


作为架构来说,之前 React15 的 Reconciler 采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack Reconciler。React16 的 Reconciler 基于 Fiber 节点实现,被称为 Fiber Reconciler;为了保持每个版本的平滑过渡,React16 的 Reconciler 也是通过递归的方式实现,通过 Fiber 中的 return 属性,指向父节点。从而达到函数递归 return 这种不破坏性的重构。

2. 一种数据结构

作为静态的数据结构来说,每个 Fiber 节点对应一个 React element,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的 DOM 节点等信息。React Fiber 就是采用链表实现的。每个 Virtual DOM 都可以表示为一个 fiber,如下图所示,每个节点都是一个 fiber。一个 fiber包括了 child(第一个子节点)、sibling(兄弟节点)、return(父节点)等属性,React Fiber 机制的实现,就是依赖于以下的数据结构。

举个例子,如下的组件结构:

function App() {
  return (
    <div>
      React App
      <span>hello</span>
      <h1>h1标签</h1>
    </div>
  )
}

对应的 Fiber 树结构:
在这里插入图片描述

3. 动态的工作单元

作为动态的工作单元来说,每个 Fiber 节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新…),Reconciler 每次去调度一个任务(单元),每次执行完一个执行单元,react 就会检查现在还剩多少时间,如果没有时间则将控制权让出去。React Fiber 与浏览器的核心交互流程如下:

在这里插入图片描述

  1. 首先 React 向浏览器请求调度执行任务,浏览器如果在一帧中还有空闲时间,会去判断是否存在待执行的任务,如果不存在任务就直接将控制权交给浏览器
  2. 如果存在就会执行对应的任务,执行完成后会判断是否还有时间,有时间且有待执行任务则会继续执行下一个任务,否则就会将控制权交给浏览器。


Fiber 可以被理解为划分一个个更小的执行单元,它是把一个大任务拆分为了很多个小块任务,一个小块任务的执行必须是一次完成的,不能出现暂停,但是一个小块任务执行完后可以移交控制权给浏览器去响应用户,从而不用像之前一样要等那个大任务一直执行完成再去响应用户。

Fiber 的结构

Fiber 结构是使用链表实现的,Fiber tree 实际上是个单链表树结构;
Fiber 节点的属性定义如下:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // 作为静态数据结构的属性
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 用于连接其他Fiber节点形成Fiber树
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  // 作为动态的工作单元的属性
  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;

  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  // 调度优先级相关
  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  // 指向该 fiber 在另一次更新时对应的 fiber
  this.alternate = null;
}

架构层面

// 指向父级Fiber节点
this.return = null;
// 指向子Fiber节点
this.child = null;
// 指向右边第一个兄弟Fiber节点
this.sibling = null;

参考前面的 Fiber 树结构可以看出,return、child、sibiling 用于连接其他 Fiber 节点形成 Fiber 树,也是一种链表结构;

​静态的数据结构

作为一种静态的数据结构,保存了组件相关的信息:

// Fiber 对应组件的类型 Function/Class/Host...
this.tag = tag;
// key属性
this.key = key;
// 大部分情况同 type,某些情况不同,比如 FunctionComponent 使用 React.memo 包裹
this.elementType = null;
// 对于 FunctionComponent,指函数本身,对于 ClassComponent,指 class,
// 对于 HostComponent,指 DOM 节点 tagName
this.type = null;
// Fiber 对应的真实 DOM 节点
this.stateNode = null;

动态的工作单元

作为动态的工作单元,Fiber中如下参数保存了本次更新相关的信息:

// 保存本次更新造成的状态改变相关信息
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;

Fiber 节点上的多个 Update 会组成链表并被包含在 fiber.updateQueue 中;updateQueue 保存着 update 对象,是更新的一个链表;数据结构如下:

const update: Update<*> = {
  eventTime,
  lane,
  suspenseConfig,
  tag: UpdateState,
  payload: null,
  callback: null,
  next: null,
};

后面会详细讲到 update 的更新流程相关;

下面两个字段保存调度优先级相关的信息。

// 调度优先级相关
this.lanes = NoLanes;
this.childLanes = NoLanes;

Fiber 架构的工作原理 - 双缓存机制

前面介绍了 Fiber 的数据结构;那如何创建链表树的,以及当有更新时它会做什么。
React 使用“双缓存”来完成 Fiber 树的构建与替换——对应着DOM树的创建与更新。

双缓存 Fiber 树


React 在 Reconciliation 过程中在有更新的情况下是存在两棵 Fiber 树;当前被刷新用来渲染用户界面的树,被称为 current Fiber 树,它用来渲染当前用户界面。每当有更新时,React 在 Reconciliation 会建立一个 workInProgress Fiber 树,它是由 React 元素中已经更新数据创建的。React 在这个 workInProgress Fiber 树上执行工作,并在下次渲染时使用这个更新的树。一旦这个 workInProgress Fiber 树被渲染到用户界面上,它就成为 current Fiber 树。


在 Fiber 的数据结构中,我们还有一个属性没有说到就是 alternate 属性;current Fiber 树与 workInProgress Fibe r 树是通过 alternate 属性连接。

  // 指向该 fiber 在另一次更新时对应的 fiber
  this.alternate = null;

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;


React 应用的根节点通过使 current 指针在不同 Fiber 树的 rootFiber 间切换来完成 current Fiber 树指向的切换。
即当 workInProgress Fiber 树构建完成交给 Renderer 渲染在页面上后,应用根节点的 current 指针指向workInProgress Fiber 树,此时 workInProgress Fiber 树就变为 current Fiber 树。

每次状态更新都会产生新的 workInProgress Fiber 树,通过 current 与 workInProgress 的替换,完成 DOM 更新。

注意 workInProgress Fiber 树的构建是在内存中完成的,构建完成之后,渲染到页面上;然后 workInProgress Fiber 树就变成了 current Fiber 树;这种在内存中构建并直接替换的技术叫做双缓存

在构建 workInProgress 树时,首屏渲染(mount)与更新(update)是不一样的,首次渲染是不存在 workInProgress Fiber 树 ,在页面存在下一次更新的时候,上次构建的 workInProgress Fiber 树已经变成 current Fiber 树;此时构建新的 workInProgress Fiber 树会与 current Fiber 树进行比对;这个过程也是我们常说的 diff 算法;

接下来我们以具体例子讲解首屏渲染(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/> 所在组件树的根节点。

之所以要区分 fiberRootNode 与 rootFiber,是因为在应用中我们可以多次调用 ReactDOM.render 渲染不同的组件树,他们会拥有不同的 rootFiber。但是整个应用的根节点只有一个,那就是 fiberRootNode。
fiberRootNode 的 current 会指向当前页面上已渲染内容对应 Fiber 树,即 current Fiber 树。

在这里插入图片描述

fiberRootNode.current = rootFiber;

由于是首屏渲染,页面中还没有挂载任何 DOM,所以 fiberRootNode.current 指向的 rootFiber 没有任何子 Fiber 节点(即 current Fiber 树为空)。

接下来进入render阶段,根据组件返回的 JSX 在内存中依次创建 Fiber 节点并连接在一起构建 Fiber 树,被称为workInProgress Fiber 树。
在构建 workInProgress Fiber 树时会尝试复用 current Fiber 树中已有的 Fiber 节点内的属性(也就是获取对应 current Fiber 树中 alternate 属性)在首屏渲染时只有rootFiber 存在对应的 current fiber(即 rootFiber.alternate)。​
在这里插入图片描述

图中右侧已构建完的 workInProgress Fiber 树在 commit 阶段渲染到页面。此时 DOM 更新为右侧树对应的样子。fiberRootNode 的 current 指针指向 workInProgress Fiber 树使其变为 current Fiber 树;这个时候 React 中只存在 current Fiber 树。
在这里插入图片描述

页面触发更新(update)时

接下来我们触发一次页面更新:点击 p 节点触发状态改变;这会开启一次新的 render 阶段并构建一棵新的workInProgress Fiber 树。

在这里插入图片描述


和 mount 时一样,workInProgress fiber 的创建可以复用 current Fiber 树对应的节点数据;这里将 alternate 属性指向旧树的同等节点;

workInProgress Fiber 树在 render 阶段完成构建后进入 commit 阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为 current Fiber 树。

在这里插入图片描述

总结

整个 Fiber 结构的工作原理就是利用双缓存机制;如果页面存在更新,在内存中构建 workInProgress Fiber 树;commit 完之后并将 fiberRootNode.current 指向 workInProgress Fiber 树;workInProgress Fiber 树变成 current Fiber 树;这样交替执行,实现了 DOM 的更新。

参考

https://juejin.cn/post/6943896410987659277
https://react.iamkasong.com/process/fiber.html
https://harttle.land/2017/08/15/browser-render-frame.html
https://github.com/hushicai/hushicai.github.io/issues/5
https://juejin.cn/post/6943896410987659277

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jiegiser#

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值