阅读 385

我所见到的React Fiber

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

为什么有Fiber

在 react15 之前,如果页面比较复杂,元素很多,而且又有频繁刷新 state 的时候,页面会出现不流畅、掉帧卡顿的情况。究其原因就是,由于 JS 是单线程的,大量的同步运算阻塞了页面的 UI 渲染。当 React 调用 setState 的时候,React 会遍历应用所有节点,计算出需要更新的差异点,然后更新 UI。整个过程是一步到位的,中间不能被打断,所有如果元素过多的时候就会出现卡顿的情况。

什么是Fiber

通常为了解决“同步阻塞”问题,一般都是有两种方案:异步或者分片处理(任务拆解)。React为了解决“同步阻塞”导致的卡顿问题,选用 分片处理 这种方案,这也就是我们所说的 Fiber 架构。

Fiber是对 React 核心算法的重构,主要就是将原来的 Stack Reconciler 替换为 Fiber Reconciler

Fiber 的目标主要有5个:

  • 能够把可中断的任务切片处理。
  • 能够调整优先级,重置并复用任务。
  • 能够在父元素与子元素之间交错处理,以支持 React 中的布局。
  • 能够在 render() 中返回多个元素。
  • 更好地支持错误边界。

PS: React 代码库概述

单链表树结构

Singly Linked List Tree Structure

Fiber 的数据结构特点是它本身是一个单链表树结构,可以进行快速的插入和删除操作。 类型定义的部分源码如下:

export type Fiber = {
  // Tag identifying the type of fiber.
  tag: WorkTag,
  // Unique identifier of this child.
  key: null | string,
  // The value of element.type which is used to preserve the identity during
  // reconciliation of this child.
  elementType: any,
  // The resolved function/class/ associated with this fiber.
  type: any,
  // The local state associated with this fiber.
  stateNode: any,

  // Conceptual aliases
  // parent : Instance -> return The parent happens to be the same as the
  // return fiber since we've merged the fiber and instance.

  // Remaining fields belong to Fiber

  // The Fiber to return to after finishing processing this one.
  // This is effectively the parent, but there can be multiple parents (two)
  // so this is only the parent of the thing we're currently processing.
  // It is conceptually the same as the return address of a stack frame.
  return: Fiber | null,

  // Singly Linked List Tree Structure.
  child: Fiber | null,
  sibling: Fiber | null,
  index: number,

  ...
  // Singly linked list fast path to the next fiber with side-effects.
  nextEffect: Fiber | null,

  // The first and last fiber with side-effect within this subtree. This allows
  // us to reuse a slice of the linked list when we reuse the work done within
  // this fiber. (存储当然任务的执行状态)
  firstEffect: Fiber | null,
  lastEffect: Fiber | null,

  lanes: Lanes,
  childLanes: Lanes,

  // This is a pooled version of a Fiber. Every fiber that gets updated will
  // eventually have a pair. There are cases when we can clean up pairs to save
  // memory if we need to.
  alternate: Fiber | null,
  ...
}
复制代码

PS:详情查看(https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js)

Fiber节点构造函数

Fiber节点的格式主要包含

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // 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;

  // Effects
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags;
  this.deletions = null;

  this.lanes = NoLanes;
  this.childLanes = NoLanes;

  this.alternate = null;
  
  ...
}
复制代码

PS:详情查看(https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiber.new.js)

实现过程

Fiber Reconciler 分为两个阶段执行。

1. render/reconciliation

render/reconciliation 生成 fiber 树,得到需要更新的差异点,这个过程是可以被打断的,

这里用到了双缓冲池技术Fiber最多只要树的两个版本。在这个阶段,除了生成 fiber tree, 在 diff 过程中构建 workInProgress tree,每一个 fiber 节点都要一个 alternate 属性(本身也是一个 fiber 结构),创建workInProgress时候优先使用current.alternate,没有的话再去创建。

workInProgress tree 的目的就是为了能够 复用reuse)fiber。

Fiber Reconcilerfiber 为工作单元,从上而下开始构建 workInProgress tree

具体过程

  1. 如果当前节点不需要更新,直接把子节点clone过来,跳到 5;要更新的话打个 tag
  2. 更新当前节点状态(props, state, context等)
  3. 调用 shouldComponentUpdate(),false的话,跳到 5
  4. 调用 render() 获得新的子节点,并为子节点创建fiber(创建过程会尽量复用现有fiber,子节点增删也发生在这里)
  5. 如果没有产生child fiber,该工作单元结束,把effect list归并到return,并把当前节点的sibling作为下一个工作单元;否则把child作为下一个工作单元
  6. 如果没有剩余可用时间了,等到下一次主线程空闲时才开始下一个工作单元;否则,立即开始做
  7. 如果没有下一个工作单元了(回到了workInProgress tree的根节点),第1阶段结束,进入pendingCommit状态

ps:参考(www.ayqy.net/blog/dive-i…)

构建 workInProgress tree 的过程就是 diff 的过程,通过 requestIdleCallback 来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),每完成一组任务,把时间控制权交还给主线程,直到下一次 requestIdleCallback 回调再继续构建 workInProgress tree

window.requestIdleCallback()会在浏览器空闲时期依次调用函数,这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这些延迟触发但关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间

为了实现上面的分组执行任务过程,React 有一个调度器 (Scheduler) 来进行任务分配。任务的优先级有六种:

  • synchronous,与之前的Stack Reconciler操作一样,同步执行
  • task,在next tick之前执行
  • animation,下一帧之前执行
  • high,在不久的将来立即执行
  • low,稍微延迟执行也没关系
  • offscreen,下一次render时或scroll时才执行

synchronous首屏(首次渲染)用,要求尽量快,不管会不会阻塞UI线程。animation 通过 requestAnimationFrame 来调度,这样在下一帧就能立即开始动画过程;后3个都是由 requestIdleCallback 回调执行的;offscreen 指的是当前隐藏的、屏幕外的(看不见的)元素

优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。

workInProgress tree构建完毕,得到的就是新的fiber tree,然后把 current 指针指向 workInProgress tree,丢掉旧的fiber tree.

部分源码如下:

// This is used to create an alternate fiber to do work on.
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  if (workInProgress === null) {
    // We use a double **buffering pooling technique** because we know that we'll
    // only ever need at most two versions of a tree. We pool the "other" unused
    // node that we're free to reuse. This is lazily created to avoid allocating
    // extra objects for things that are never updated. It also allow us to
    // reclaim the extra memory if needed.
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;

    // fiber 与 workInProgress 互相持有引用

    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps;
    // Needed because Blocks store data on type.
    workInProgress.type = current.type;

    // We already have an alternate.
    // Reset the effect tag.
    workInProgress.flags = NoFlags;

    // The effects are no longer valid.
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;

    if (enableProfilerTimer) {
      // We intentionally reset, rather than copy, actualDuration & actualStartTime.
      // This prevents time from endlessly accumulating in new commits.
      // This has the downside of resetting values for different priority renders,
      // But works for yielding (the common case) and should support resuming.
      workInProgress.actualDuration = 0;
      workInProgress.actualStartTime = -1;
    }
  }

  // Reset all effects except static ones.
  // Static effects are not specific to a render.
  workInProgress.flags = current.flags & StaticMask;
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;

  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;

  // Clone the dependencies object. This is mutated during the render phase, so
  // it cannot be shared with the current fiber.
  const currentDependencies = current.dependencies;
  workInProgress.dependencies =
    currentDependencies === null
      ? null
      : {
          lanes: currentDependencies.lanes,
          firstContext: currentDependencies.firstContext,
        };

  // These will be overridden during the parent's reconciliation
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;

  if (enableProfilerTimer) {
    workInProgress.selfBaseDuration = current.selfBaseDuration;
    workInProgress.treeBaseDuration = current.treeBaseDuration;
  }


  return workInProgress;
}
复制代码

在这个阶段会被执行的生命周期包含:

componentWillMount(已废弃)
componentWillReceiveProps(已废弃)
getDerivedStateFromProps
shouldComponentUpdate
componentWillUpdate (已废弃)
复制代码

2. commit

commit 批量更新差异点 (effect list),这个过程不能被打断。

在这个阶段执行的生命周期:

componentDidMount
componentDidUpdate
componentWillUnmount
复制代码

fiber树结构图

image.png (图片来源于网络)

常见问题

fiber 如何中断/断点恢复?

中断:检查当前正在处理的工作单元,保存当前成果(firstEffect, lastEffect),修改 tag 标记一下,迅速收尾并再开一个requestIdleCallback,下次有机会再做

断点恢复:下次再处理到该工作单元时,看 tag 是被打断的任务,接着做未完成的部分或者重做

结语

如果这篇文章帮到了你,欢迎点赞👍和关注⭐️。

文章如有错误之处,希望在评论区指正🙏🙏。

附:

文章分类
前端
文章标签