React Fiber剖析

react-fiber-logo.jpeg

React自从2013年5月开源以来,一路披襟斩棘到前端最热门框架之一,框架本身具有以下特性。

  • Declarative(声明式)
  • Component-Based(组件式)
  • Learn Once, Write Anywhere(多端渲染式)
    除此之外还有快速高效等特点,主要得益于Virtual Dom的应用,虚拟Dom是一种HTML DOM节点的抽象描述,存在JS中的结构对象中,当渲染时通过Diff算法,找到需要变更的节点进行更新,这样就节省了不必要的更新。

React快速响应主要制约于CPU瓶颈,比如以下栗子所示:

function App() {
const len = 3000;
  return (
    
    {Array(len).fill(0).map((_, i) =>
  • {i}
  • )}
} const rootEl = document.querySelector("#root"); ReactDOM.render(, rootEl);

当需要被渲染的节点很多时,有存在大量的JS计算,因为GUI渲染线程JS执行线程是互斥的,所以在JS计算的时候就会停止浏览器界面渲染行为,导致页面感觉卡顿。

主流浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次,也就说渲染一帧的时间必须控制在16ms内才能保证不掉帧。

这段时间内需要完成以下操作:

  • 脚本执行(JavaScript)
  • 样式计算(CSS Object Model)
  • 布局(Layout)
  • 重绘(Paint)
  • 合成(Composite)
    JS->Style->Layout->Paint->Composite过程
    既然JS执行比较耗时,能不能中断或暂停JS的执行,把执行权交回给渲染线程呢?
    首先看一下React是怎么去做这事的
 // react/packages/scheduler/src/forks/SchedulerHostConfig.default.js
 // Scheduler periodically yields in case there is other work on the main
  // thread, like user events. By default, it yields multiple times per frame.
  // It does not attempt to align with frame boundaries, since most tasks don't
  // need to be frame aligned; for those that do, use requestAnimationFrame.
  let yieldInterval = 5;
  let deadline = 0;

从源码中可以看到,React每次会利用这部分时间(5ms)更新组件,当超过这个时间React就会将执行权就还给浏览器由浏览器自主分配执行权,React本身则等待下一帧时间来继续被中断的工作,这就引入了一个时间切片的概念。将耗时的长任务拆分到每一帧中,一次执行小块任务。总结来说就是将 同步的更新变成可中断的异步更新

React v15 Stack Reconciler

ReactDOM.render(, rootEl);

React DOM将传递给Reconciler,此时Reconciler将会检查App是 函数 or

  • 【函数】 -> App(props)
  • 【类】 -> new App(props) 来实例化 App, 并调用生命周期方法 componentWillMount(),之后调用 render() 方法来获取渲染的元素

tips: 面试过程中通常会问函数组件和类组件,两者是否都被实例化?答案就在上面

此过程是基于树的深度遍历的递归过程(遇到自定义组件就会一直的递归下去,直到最原始的HTML标签),Stack Reconciler 的递归一旦进入调用栈就无法中断或暂停,如果当组件嵌套很深或数量极多,在16ms内无法完成就势必造成浏览器丢帧导致卡顿。
刚在上面也提过解决方案就是将 同步的更新变成可中断的异步更新,但15版本架构不支持异步更新,所以React团队决定撸起袖子重写,折腾了两年多终于在2017/3发布了可用版本。

React Fiber

在首次渲染中构建出虚拟dom树,后续更新时(setState)通过diff虚拟dom树得到dom change,最后将dom change应用到真实dom树中,Stack Reconciler自顶向下递归(mount/update)无法中断导致主线程上的布局/动画/交互响应无法及时得到处理,引起卡顿。

这些问题Fiber Reconciler 能够解决。

Fiber原意纤维,工作最小单元,每次通过ReactDOM.render首次构建时都会生成一个FiberNode,接下来具体看下FiberNode结构。

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;  // FiberNode类型,目前总有25种类型,常用的就是FunctionComponent 和 ClassComponent
  this.key = key; //和组件Element中的key一致
  this.elementType = null;
  this.type = null;  //Function|String|Symbol|Number|Object
  this.stateNode = null; //FiberRoot|DomElement|ReactComponentInstance等绑定的其他对象

  // Fiber
  this.return = null; // FiberNode|null 父级FiberNode
  this.child = null; // FiberNode|null 第一个子FiberNode
  this.sibling = null;// FiberNode|null 相邻的下一个兄弟节点
  this.index = 0;  //当前父fiber中的位置

  this.ref = null; //和组件Element中的ref一致

  this.pendingProps = pendingProps; // Object 新的props
  this.memoizedProps = null; // Object 处理后的新props
  this.updateQueue = null; // UpdateQueue 即将要变更的状态
  this.memoizedState = null;  //Object 处理后的新state
  this.dependencies = null;

  this.mode = mode; // number
  // 普通模式,同步渲染,React15-16的生产环境使用
  // 并发模式,异步渲染,React17的生产环境使用
  // 严格模式,用来检测是否存在废弃API,React16-17开发环境使用
  // 性能测试模式,用来检测哪里存在性能问题,React16-17开发环境使用

  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null; // render阶段的diff过程检测到fiber的子节点如果有需要被删除的节点

  this.lanes = NoLanes; //如果fiber.lanes不为空,则说明该fiber节点有更新
  this.childLanes = NoLanes; //判断当前子树是否有更新的重要依据,若有更新,则继续向下构建,否则直接复用已有的fiber树

  this.alternate = null; //FiberNode|null 候补节点,缓存之前的Fiber节点,与双缓存机制相关,后续讲解

}

所有fiber对象都是FiberNode实例,通过tag来标识类型。通过createFiber初始化FiberNode节点,代码如下

const createFiber = function(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
): Fiber {
  // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
  return new FiberNode(tag, pendingProps, key, mode);
};

Fiber解决这个问题的解法是把渲染/更新过程拆分成一系列小任务,每次执行一小块,再看是否有剩余时间继续下一个任务,有则继续,无则挂起,将执行线程归还。

Fiber Tree

通过虚拟dom树,react会再创建一个Fiber Tree,不同的Element类型对应不同类型的Fiber Node,在后续的更新过程中每次重新渲染都会重新创建Element,但是Fiber不会重新创建,只会更新自身属性。
顾名思义,通过多个Fiber Node组成了一个Fiber Tree,也是为了满足Fiber增量更新的特性才拓展出了Fiber Tree结构。

Fiber Tree.png

首先每个节点是统一的,会有两个属性FirstChildNextSibiling,第一个指向节点第一个儿子节点,第二个指向下一个兄弟节点,Fiber这种单链表结构就可以把整个树串联起来。同时Fiber Tree在Instance层又新增了额外三个实例:

  • effect:每个workInProgress tree节点上都有一个effect list 存放diff结果,更新完毕后updateQueue进行收集
  • workInProgress: reconcile过程中的快照,工作过程节点,用户不可见
  • fiber:用来描述增量更新所需的上下文信息

这里我们着重来理解一下 workInProgress到底起了什么作用?首先通过代码来看下它是如何被创建的

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
  // 以下两句很关键
  workInProgress.alternate = current;
  current.alternate = workInProgress;
  // do something else ...
  } else {
  // do something else ...
  }
 // do something else ...
  return workInProgress;
}

首先workInProgress一个Fiber节点,当前节点的alternate为空时,通过createFiber创建,每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换完成dom更新, 简单来说当workInProgress Tree内存中构建完成后直接替换Fiber Tree的做法,就是刚刚提到的双缓冲机制

workInProgress-mount

当内存中的workInProgress树直接构建完成后,直接替换了页面需要渲染的Fiber树,这是mount的过程。
当页面其中一个node节点发生变更时,会开启一次新的render阶段并构建一颗心的workInProgress树,
这里有个优化点就是 因为每个node节点都有一个alternate属性互相指向,在构建时会尝试复用当前current Fiber树已有的节点内属性,是否复用取决于diff算法判断。


workInProgress-update

在更新过程中,React在filbert tree中实际发生改变的fiber上创建effect,所有effect构成effect list链表,在commit阶段执行,实现了只对实际发生改变的fiber做dom更新,避免了遍历整个fiber tree造成性能浪费。每当一个Fiber节点的flags字段不为NoFlags时,就会把此Fiber节点添加到effect list中,根据每一个effect的effectTag类型执行对应的dom树更改。

递归Fiber节点

Fiber架构下的每个节点都会经历两个过程,即beginWork/completeWork。

1、beginWork
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
// do something else
}
  • current: 当前组件上一次更新的Fiber节点,workInProgress.alternate
  • workInProgress: 当前组件内存的Fiber节点
  • renderlanes: 相关优先级
    由于双缓存机制的存在,我们可以通过current === null 来判断组件是处于mount还是uplate,当mount时会根据fiber.tag创建不同类型的子Fiber节点,当update时 didReceiveUpdate === false就可以直接复用前一次更新的子Fiber节点,具体判断如下:
if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        // do something else
      }
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderLanes,
      );
    } else {
      didReceiveUpdate = false;
    }
  } else {
    didReceiveUpdate = false;
  }
2、completeWork
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
     // do something else
      return null;
    }
    case HostRoot: {
      // do something else
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // do something else
      return null;
    }
  // do something else
}

传入参数和beginWork一致,不做过多讲解,completeWork会根据tag不同调用不同的处理逻辑。对于处理的当前节点是mount还是update阶段同样可以使用current === null 来做判断。由于completeWork属于“归”阶段的函数,每次调用appendAllChildren都会将已生成的子孙节点插入当前生成的dom节点,这样就一个完整的dom树了。

3、effectList

每个执行完completeWork并且存在effectTag的Fiber节点都会保存在effectList单向链表中,同时effectList第一个和最末个Fiber节点会分别保存在fiber.firstEffect /fiber.lastEffect属性中。


effectList.png

effectList使得commit阶段只需要遍历effectList就可以了,提高了运行性能, 至此 render阶段告一段落。

写在最后

我觉得React Fiber是一种解决问题的理念架构,从React16架构来说分为三层:
Scheduler/Reconciler/Renderer
它利用浏览器的空闲时间完成循环模拟递``归过程,所有操作都在内存中进行,只有所有组件完成Rconciler工作,才会走Renderer一次渲染展示,提升效率。
篇幅不长,知识点零零总总,性能优化没有最好,只有更好,所以我们一直在路上...

你可能感兴趣的:(React Fiber剖析)