⚡「React」来讲讲 React Fiber 是个啥东西

763 阅读11分钟

一 前置知识

我们知道,在浏览器中,页面是一帧一帧绘制出来的,渲染的帧率与设备的刷新率保持一致。大多数设备的屏幕刷新率为1s 60次,当每秒内绘制的帧数(FPS)超过60时,页面渲染是流畅的;而当FPS小于60时,会出现一定程度的卡顿现象。

1000ms / 60 = 16.7ms

浏览器是多进程的。

  • 浏览器主进程
  • GPU进程
  • 第三方插件进程
  • 浏览器渲染进程

浏览器渲染进程又是多线程的。

  • GUI渲染线程
  • JS引擎线程
  • 定时触发线程
  • 事件触发线程
  • 异步http请求线程

我们知道,JS是一个单线程的语言。浏览器中UI渲染线程和js线程互斥,执行js时无法进行UI渲染,长时间执行js导致UI渲染线程长时间挂起,页面就会卡顿甚至直接卡死。

完整的一帧都做了什么事情。

image.png

1 用户进行输入或者执行点击等操作

2 执行事件的回调

3 处理开始帧,例如resize、scroll等

4 在绘制之前执行requestAnimationFrame(请求动画帧)

5 Layout 计算布局,位置信息

6 重绘,根据尺寸和位置进行元素的内容填充

7 前六个阶段处理完成之后,可能用时到不了16.7ms,仅仅只用了4ms,这时候会有一个空闲阶段。这时候是可以去执行requestIdleCallback里的注册任务。(下面再去讲这个回调函数,它是React Fiber 的实现基础)

二 React

JSX 是如何转换的

const element = (
  <div>
    <div>我是测试demo</div>
    <div>我是测试demo</div>
    <div>我是测试demo</div>
  </div>
)

babel 跳转

React 是如何工作的

import React from "react";
import ReactDOM from "react-dom";

const element = (
  <div>
    <div>我是测试demo</div>
    <div>我是测试demo</div>
    <div>我是测试demo</div>
  </div>
);

console.log(element);

ReactDOM.render(element, document.getElementById("root"));

下面的就是我们常说的虚拟DOM

我们简化一下

const element = {
  type: 'div',
  props: {
    children: [
      {
        type: 'div',
        props: {
          children: '我是测试demo'
        }
      },
      {
        type: 'div',
        props: {
          children: '我是测试demo'
        }
      },
      {
        type: 'div',
        props: {
          children: '我是测试demo'
        }
      }
    ]
  }
}

一个React组件的渲染主要经历两个阶段

调度阶段:用新的数据生成一个新的树,通过 diff 算法遍历旧的树,快速找出需要更新的元素,放到更新队列中,得到新的更新队列。

渲染阶段:遍历更新队列,通过调用宿主环境api,实际更新渲染对应的元素。

React 15

在 react16 引入 Fiber 架构之前,react使用的架构是《栈调和》(stack reconciler),react 会采用递归对比虚拟DOM树,通过 diff算法 找出需要变动的节点,然后同步更新它们,这个过程 react 称为reconcilation(协调)。也就是通过例如react-dom类库使虚拟dom与真实的dom同步,这个过程就是协调。

在reconcilation 期间,react 会一直占用浏览器资源,会导致用户触发的事件得不到响应。react 15 是如何将 JSX 渲染到页面上的。通过深度优先遍历,遍历自上到下进行遍历。

遍历的顺序 A1 -> B1 -> C1 -> C2 -> B2 -> C3 -> C4

这种遍历是递归调用,执行栈会越来越深,而且不能中断,中断后就不能恢复了。递归如果非常深,就会十分卡顿。如果递归花了100ms,则这100ms浏览器是无法响应的,而我们感觉流畅的最大时间也就是16.7ms,代码执行时间越长卡顿越明显。传统的方法存在不能中断和执行栈太深的问题。

一个小🌰

谢尔宾斯基三角形:百度百科

  1. 在每一帧渲染之前(通过requestAnimationFrame方法)给最外层的div设置一个缩放的transform,也就是让整个div开启一个不停变大缩小的动画。
  2. 设置一个定时器,每过1秒钟就改变一次组件的状态,也就是执行一次setState,并且demo中的所有子组件都会受到影响并render一次。

stack 掉帧 claudiopro.github.io/react-fiber…

fiber 不掉帧 claudiopro.github.io/react-fiber…

三 React Fiber

为了解决react15中组件render过程耗时过多,或者参与调和阶段的虚拟DOM节点过多的问题,那么react团队在react16版本提出了fiber架构和scheduler任务调度。fiber架构的目的是【能够独立执行每个虚拟DOM的调和阶段】,而不是每次执行整个虚拟DOM树的调和阶段。

什么是 React Fiber

Fiber的英文含义叫做"纤维",计算机领域中有两个大家很熟悉的概念:进程(Process)和线程(Thread)意思就是指的比Thread更细的概念,也就是比线程控制的更加精密的并发处理结构。

React Fiber并不是所谓的纤程(微线程)那种概念。而是一种基于浏览器的单线程调度算法。背后其实是基于 requestIdleCallback 这个API,Fiber是一种将 recocilation (递归 diff),拆分成无数个小任务的算法;它随时能够停止,恢复。停止恢复的时机取决于当前的一帧(16ms)内,还有没有足够的时间允许计算。

四 React Fiber 实现原理

一个执行单元

Fiber 可以理解为一个执行的单元,如下图所示,浏览器在每一帧都有一个空闲时间,react正是利用这个空闲时间去执行分片后优先级较高的任务。一旦空闲时间结束之后把执行权再交给浏览器。

React 和浏览器配合调度关系图

requestAnimationFrame

react fiber 中使用了 requestAnimationFramewindow.requestAnimationFrame 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行

如果我想浏览器在每一帧中,将页面 div 元素的宽度变长1px,到达 100 px 结束。例如这样

const element = document.getElementById('some-element-you-want-to-animate');
let start;

function step(timestamp) {
  if (start === undefined)
    start = timestamp;
  const elapsed = timestamp - start;
  
  //这里使用`Math.min()`确保元素刚好停在200px的位置。
  element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
  
  if (elapsed < 2000) { // 在两秒后停止动画
    window.requestAnimationFrame(step);
  }
}

window.requestAnimationFrame(step);

requestIdleCallback

react fiber 中的基础API,requesetIdleCallback。这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先进先调用的顺序执行,然而,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序。

由于requestIdleCallback利用的是帧的空闲时间, 所以有可能出现浏览器一直处于繁忙状态, 导致回调一直无法执行, 那这时候就需要在调用requestIdleCallback的时候传递第二个配置参数timeout了.

const workLoop = (deadline) => {
  console.log(`此帧的剩余时间为: ${deadline.timeRemaining()}`);
  // 如果此帧剩余时间大于0或者已经到了定义的超时时间(上文定义了timeout时间为1000,到达时间时必须强制执行),且当时存在任务,则直接执行这个任务
  // 如果没有剩余时间,则应该放弃执行任务控制权,把执行权交还给浏览器
  while (
    (deadline.timeRemaining() > 0 || deadline.didTimeout) &&
    taskQueue.length > 0
  ) {
    performUnitWork();
  }
  
  // 如果还有未完成的任务,继续调用requestIdleCallback申请下一个时间片
  if (taskQueue.length > 0) {
    requestIdleCallback(workLoop, { timeout: 1000 });
  }
};
requestIdleCallback(workLoop, { timeout: 1000 });

拓展资料:requestIdleCallback

一种数据结构

源码:react fiber 数据结构

协调的过程

fiber reconciler 在执行的过程中,从根节点开始渲染和调度的过程可以分为两个阶段:render,commit

  • render 阶段,生成 fiber 树,得出需要更新的节点信息,这一步是渐进的过程,是可以被中途打断的。
  • commit 阶段,讲需要更新的节点一次性批量更新,这个过程是不可以被打断的。

render

遍历

在 render 阶段时,react 会采用深度优先遍历,对 fiber 树进行遍历,其中每个Virtual DOM 都可以表示为一个 fiber。每一个节点都是一个 fiber。一个 fiber 包含child、sibling、return。下图的两个 div 都是一个 fiber。

  • return:指向父节点,若没有父fiber则为 null
  • child:指向第一个子 fiber,若没有任何子 fiber 则为 null
  • sibling:指向下一个兄弟 fiber,若没有下一个兄弟 fiber 则为 null
<App>
  <div />
  <input />
  <List>
    <div />
    <div />
  </List>
</App>

记住一个规则:优先儿子,再兄弟。

双缓存Fiber树

什么是 双缓存?

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

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

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

即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。

在首次执行ReactDOM.render的时候,会创建一个 fiberRootNode,他是整个应用的根。

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

mount

首次执行 ReactDOM.render 的时候,页面还未挂载 DOM。current tree是空的。

update

workInProgress fiber的创建可以复用current Fiber树对应的节点数据。

effect list

与react15不同的是,fiber采用链表树的形式实现的,我们刚刚了解了在render阶段,react 采用深度优先遍历对 fiber 树进行遍历。把每个有副作用的 fiber 筛选出来,构建一个只带有副作用的effect list链表。

这个链表包括 firstEffect、nextEffect、lastEffect。


FiberNode:源码

// packages/react-reconciler/src/ReactInternalTypes.js

export type Fiber = {|
  // 作为静态数据结构,存储节点 dom 相关信息
  tag: WorkTag, // 组件的类型,取决于 react 的元素类型
  key: null | string,
  elementType: any, // 元素类型
  type: any, // 定义与此fiber关联的功能或类。对于组件,它指向构造函数;对于DOM元素,它指定HTML tag
  stateNode: any, // 真实 dom 节点
  
  // fiber 链表树相关, 主要
  return: Fiber | null, // 父 fiber
  child: Fiber | null, // 第一个子 fiber
  sibling: Fiber | null, // 下一个兄弟 fiber
  index: number, // 在父 fiber 下面的子 fiber 中的下标
  
  ref:
    | null
    | (((handle: mixed) => void) & {_stringRef: ?string, ...})
    | RefObject,
    
  // 工作单元,用于计算 state 和 props 渲染
  pendingProps: any, // 本次渲染需要使用的 props
  memoizedProps: any, // 上次渲染使用的 props
  updateQueue: mixed, // 用于状态更新、回调函数、DOM更新的队列
  memoizedState: any, // 上次渲染后的 state 状态
  dependencies: Dependencies | null, // contexts、events 等依赖
  
  mode: TypeOfMode,
  
  // 副作用相关
  flags: Flags, // 记录更新时当前 fiber 的副作用(删除、更新、替换等)状态
  subtreeFlags: Flags, // 当前子树的副作用状态
  deletions: Array<Fiber> | null, // 要删除的子 fiber
  nextEffect: Fiber | null, // 下一个有副作用的 fiber
  firstEffect: Fiber | null, // 指向第一个有副作用的 fiber
  lastEffect: Fiber | null, // 指向最后一个有副作用的 fiber 
  
  // 优先级相关
  lanes: Lanes,
  childLanes: Lanes,
  
  alternate: Fiber | null, // 指向 workInProgress fiber 树中对应的节点
  
  actualDuration?: number,
  actualStartTime?: number,
  selfBaseDuration?: number,
  treeBaseDuration?: number,
  _debugID?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
  _debugNeedsRemount?: boolean,
  _debugHookTypes?: Array<HookType> | null,
|};

WorkTag:源码

组件的类型,取决于 react 的元素类型

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;

commit

commit阶段主要做的是,根据 fiber 的effect list的 effectTag 去更新视图。(新增、更新、删除)。源码在此

React 15 vs React 16

核心对比

StackFiber
版本15.x16.x
数据结构数组(树)链表
任务调度不能暂停可暂停、中断、恢复

抽象对比

stack

fiber

效果对比

stack 掉帧 claudiopro.github.io/react-fiber…

fiber 不掉帧 claudiopro.github.io/react-fiber…

五 总结

  • 对大型复杂任务进行分片
  • 对任务进行优先级划分,优先调度高优先级的任务
  • 调度过程中,可以对任务进行挂起、恢复、终止等操作

react fiber 已经有一定的了解了,让我们一起来实现一个Mini-React吧。

学习react源码是极为漫长的事情,我们需要先大致了解一遍react执行的整个链路,然后再去逐个去学习每个知识点(createElement、fiber、hooks等)。

六 写在最后

  • 如有写的不对的地方希望大家提醒。
  • 感谢大家看到这里,这次分享的是React Fiber,希望可以帮助到有需要的同学。
  • 如果您觉得这篇文章有帮助到您的的话不妨🍉关注+点赞+收藏+评论+转发🍉支持一下哟~~😛您的支持就是我更新的最大动力。

参考

react技术揭秘

走进React Fiber世界