React Fiber 架构 - 文章教程

React Fiber 架构

发布于 2021-01-15 字数 17897 浏览 822 评论 0

React 的理念

我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。 ——官网

快速响应即:速度快,响应自然

响应快

由于语法的灵活,在编译时无法区分可能变化的部分。所以在运行时,React需要遍历每个元素,判断其数据是否更新。基于以上原因,相比于VueAngular,缺少编译时优化手段的React为了速度快需要在运行时做出更多努力。

  • 使用PureComponentReact.memo构建组件
  • 使用shouldComponentUpdate生命周期钩子
  • 渲染列表时使用key
  • 使用useCallbackuseMemo缓存函数和变量

由开发者来显式的告诉React哪些组件不需要重复计算、可以复用。

响应自然

将人机交互研究的结果整合到真实的 UI 中

同步的更新变为可中断的异步更新

React15 架构

React 15 的架构可以分为两层:

  • Recociler(协调器):负责找出变化的组件
  • Renderer(渲染器):负责将变化的组件渲染到页面

Reconciler(协调器)

React中可以通过this.setStatethis.forceUpdateReactDOM.render等API触发更新。

每当有更新发生时,Reconciler会做如下工作:

  • 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
  • 将虚拟DOM和上次更新时的虚拟DOM对比
  • 通过对比找出本次更新中变化的虚拟DOM
  • 通知Renderer将变化的虚拟DOM渲染到页面上(找出需要重绘或重排的元素,告诉浏览器。浏览器根据相关的更新,重新计算 DOM Tree,重绘页面。)

Renderer(渲染器)

由于React支持跨平台,所以不同平台有不同的Renderer。我们前端最熟悉的是负责在浏览器环境渲染的Renderer —— ReactDOM

除此之外,还有:

  • ReactNative渲染器,渲染App原生组件
  • ReactTest渲染器,渲染出纯Js对象用于测试
  • ReactArt渲染器,渲染到Canvas, SVG 或 VML (IE8)

在每次更新发生时,Renderer接到Reconciler通知,将变化的组件渲染在当前宿主环境。

React15架构的缺点

  • Reconciler中,mount的组件会调用mountComponentupdate的组件会调用updateComponent。这两个方法都会递归更新子组件。
  • React15 的调度策略 – Stack reconcile。这个策略像函数调用栈一样,会深度优先遍历所有的 Virtual DOM 节点,进行Diff。它一定要等整棵 Virtual DOM 计算完成之后,才将任务出栈释放主线程。所以,在浏览器主线程被 React 更新状态任务占据的时候,用户与浏览器进行任何的交互都不能得到反馈,只有等到任务结束,才能突然得到浏览器的响应。【同步更新,不可中断
  • React 这样的调度策略对动画的支持也不好。如果 React 更新一次状态,占用浏览器主线程的时间超过 16.6 ms,就会被人眼发现前后两帧不连续,给用户呈现出动画卡顿的效果。【主流的浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次。我们知道,JS可以操作DOM,GUI渲染线程JS线程是互斥的。所以JS脚本执行浏览器布局、绘制不能同时执行。】
  • Reconciler采用递归的方式创建虚拟DOM,递归过程是不能中断的。如果组件树的层级很深,递归会占用线程很多时间,造成卡顿。

React 16架构

React16架构可以分为三层:

  • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
  • Reconciler(协调器)—— 负责找出变化的组件
  • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

Scheduler(调度器)

既然我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。

React放弃使用 requestIdleCallback原因:【浏览器对 requestIdleCallback requestAnimationFrame 实现了类似功能】

  • 浏览器兼容性
  • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的requestIdleCallback触发的频率会变得很低

基于以上原因,React实现了功能更完备的requestIdleCallbackpolyfill,这就是Scheduler。除了在空闲时触发回调的功能外,Scheduler还提供了多种调度优先级供任务设置。

Scheduler 是独立于 React 的库

Reconciler(协调器)

React15React16,协调器(Reconciler)重构的一大目的是:将老的同步更新的架构变为异步可中断更新

异步可中断更新可以理解为:更新在执行过程中可能会被打断(①有其他更高优先级任务需要先更新②当前帧没有剩余时间),当可以继续执行时恢复之前执行的中间状态。

/** @noinline */
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

那么React16是如何解决中断更新时DOM渲染不完全的问题呢?

在React16中,ReconcilerRenderer不再是交替工作【React15架构的Reconciler和Renderer是交替工作的】。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,类似这样:

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

整个SchedulerReconciler的工作都在内存中进行,不会更新到DOM上面。【所以即使反复中断,用户也不会看见更新不完全的DOM】只有当所有组件都完成Reconciler的工作,才会统一交给Renderer

Reconciler 内部采用了 Fiber 的结构。

Renderer(渲染器)

Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。

Fiber

Fiber 架构的心智模型:参考Fiber 架构的心智模型、代数效应入门

Fiber 的含义

Fiber 包含三层含义:

  1. 作为架构来说,之前React15Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack ReconcilerReact16Reconciler基于Fiber节点实现,被称为Fiber Reconciler。每个Fiber节点有个对应的React element,多个Fiber节点通过如下三个属性连接成树。
    // 指向父级Fiber节点
    this.return = null;
    // 指向子Fiber节点
    this.child = null;
    // 指向右边第一个兄弟Fiber节点
    this.sibling = null;
  2. 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的DOM节点等信息。
    // 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;
  3. 作为动态的工作单元来说,每个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;

另外,如下两个字段保存调度优先级相关的信息,会在讲解Scheduler时介绍。

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

Fiber 工作原理

Fiber节点可以保存对应的DOM节点。相应的,Fiber节点构成的Fiber树就对应DOM树。那么如何更新DOM呢?这需要用到被称为“双缓存”的技术。

双缓存是什么

  • 当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。
  • 如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。
  • 为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。
  • 这种在内存中构建并直接替换的技术叫做双缓存
  • React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新。

双缓存Fiber树

React中最多会同时存在两棵Fiber树当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树

current Fiber树中的Fiber节点被称为current fiberworkInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

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

React应用的根节点通过current指针在不同Fiber树rootFiber间切换来实现Fiber树的切换。

workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树

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

Fiber树的构建与替换过程

Fiber树的构建与替换过程,这个过程伴随着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/>所在组件树的根节点**。

之所以要区分fiberRootNoderootFiber,是因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode

fiberRootNodecurrent会指向当前页面上已渲染内容对应对Fiber树,被称为current Fiber树

fiberRootNode.current = rootFiber;

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

React Fiber 架构

接下来进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。(下图中右侧为内存中构建的树,左侧为页面显示的树)

在构建workInProgress Fiber树时会尝试复用current Fiber树中已有的Fiber节点内的属性,在首屏渲染时只有rootFiber存在对应的current fiber(即rootFiber.alternate)。

React Fiber 架构

图中右侧已构建完的workInProgress Fiber树commit阶段渲染到页面。

此时DOM更新为右侧树对应的样子。fiberRootNodecurrent指针指向workInProgress Fiber树使其变为current Fiber 树

React Fiber 架构

update 时

1.接下来我们点击 p节点 触发状态改变,这会开启一次新的 render阶段 并构建一棵新的 workInProgress Fiber 树

React Fiber 架构

mount 时一样,workInProgress fiber的创建可以复用 current Fiber树 对应的节点数据。

这个决定是否复用的过程就是Diff算法,后面章节会详细讲解

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

React Fiber 架构

Fiber Reconciler 与 Stack Reconciler 的不同

Fiber 是一种轻量的执行线程,同线程一样共享定址空间,线程靠系统调度,并且是抢占式多任务处理,Fiber 则是自调用,协作式多任务处理。

首先,使用协作式多任务处理任务。将原来的整个 Virtual DOM 的更新任务拆分成一个个小的任务。每次做完一个小任务之后,放弃一下自己的执行将主线程空闲出来,看看有没有其他的任务。如果有的话,就暂停本次任务,执行其他的任务,如果没有的话,就继续下一个任务。

整个页面更新并重渲染过程分为两个阶段。

  1. Reconcile 阶段。此阶段中,依序遍历组件,通过 diff 算法,判断组件是否需要更新,给需要更新的组件加上 tag。遍历完之后,将所有带有 tag 的组件加到一个数组中。这个阶段的任务可以被打断。
  2. Commit 阶段。根据在 Reconcile 阶段生成的数组,遍历更新 DOM,这个阶段需要一次性执行完。如果是在其他的渲染环境 – Native,硬件,就会更新对应的元素。

所以之前浏览器主线程执行更新任务的执行流程就变成了这样。

React Fiber 架构

其次,对任务进行优先级划分。不是每来一个新任务,就要放弃现执行任务,转而执行新任务。与我们做事情一样,将任务划分优先级,只有当比现任务优先级高的任务来了,才需要放弃现任务的执行。比如说,屏幕外元素的渲染和更新任务的优先级应该小于响应用户输入任务。若现在进行屏幕外组件状态更新,用户又在输入,浏览器就应该先执行响应用户输入任务。浏览器主线程任务执行流程如下图所示。

React Fiber 架构

使用了 ReactFiber 去渲染整个页面,ReactFiber 会将整个更新任务分成若干个小的更新任务,然后设置一些任务默认的优先级。每执行完一个小任务之后,会释放主线程。

需要考虑的问题:

  • 比如说,task 按照优先级之后,可能低优先级的任务永远不会执行,称之为 starvation;
  • 比如说,task 有可能被打断,需要重新执行,那么某些依赖生命周期实现的业务逻辑可能会受到影响。

React Fiber 也是带来了很多的好处的。

  • 比如说,增强了某些领域的支持,比如动画、布局和手势;
  • 比如说,在复杂页面,对用户的反馈会更及时,应用的用户体验会变好,简单页面看不到明显的差异;
  • 比如说,api 基本上没有变化,对现有项目很友好。

如果你对这篇文章有疑问,欢迎到本站 社区 发帖提问或使用手Q扫描下方二维码加群参与讨论,获取更多帮助。

扫码加入群聊

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

目前还没有任何评论,快来抢沙发吧!

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

2891 文章
评论
84935 人气
更多

推荐作者

时光倒影

文章 0 评论

qq_YyjhCs

文章 0 评论

三人与歌

文章 0 评论

┼──瘾||

文章 1 评论