React 源码解读之 useFiber

428 阅读4分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

react 版本:v17.0.3

在React的diff过程中,无论是对单节点的比较还是对多节点的比较,只要找到key和elementType相同的旧节点,就会通过useFiber() 函数复用旧节点。下面,我们来解读一下 useFiber 方法。

useFiber

// react-reconciler/src/ReactChildFiber.new.js

// 复用旧节点
function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {
    
    // 基于旧fiber和新的props.children 克隆生成一个新的 fiber,从而复用旧fiber节点
    const clone = createWorkInProgress(fiber, pendingProps);
    clone.index = 0;
    clone.sibling = null;
    return clone;
}

可以看到,useFiber 做的事情十分简单,就是调用 createWorkInProgress() 函数,基于旧fiber节点和新内容的 props,克隆生成一个新的fiber节点,从而实现旧节点的复用。并在返回新的fiber节点前将其 index属性重置为0,sibling属性重置为 null。

在调用 createWorkInProgress 克隆旧节点时,通过 双缓冲 的方式来完成旧节点的复用。下面我们来看看 createWorkInProgress 函数。

createWorkInProgress

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.
    
    // 使用 双缓冲池技术是因为 只需要最多两个版本的 fiber 树,就可以重用另一个版本的fiber树上的节点
    // 因为一棵 fiber 树顶多有两个版本,所以当某一 fiber 节点不更新时,在更新 fiber 树的时候,
    // 不会去重新创建跟之前一样的 fiber 节点,而是从另一个版本的 fiber 树上重用它
    
    // 新建 fiber 节点
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode,
    );
    
    // 复用旧节点的属性
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;

    // ...

    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps;
    // Needed because Blocks store data on type.
    // 复用旧节点的 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;
}

在 React 中最多会同时存在两个 Fiber 树。当前屏幕上显示的内容对应的 Fiber 树称为 current Fiber 树,正在内存中构建的 Fiber 树称为 workInProgress Fiber 树。current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

// current 为当前屏幕上显示的内容对应的 Fiber树
// workInProgress 为内存中正在构建的 Fiber树
workInProgress.alternate = current;
current.alternate = workInProgress;

在 createWorkInProgress 函数中,首先获取 current Fiber 树 的副本作为workInProgress Fiber 树:

let workInProgress = current.alternate;

如果 workInProgress 为 null,调用 createFiber() 函数,基于旧节点的 tag、key、mode属性和新内容的props,构建一个新的fiber节点,然后复用旧节点的其它属性,并将当前的 current Fiber 树 和 workInProgress Fiber 树通过 alternate 属性连接起来。

如果 workInProgress 不为 null,则直接将current 的副本作为workInProgress ,然后复用旧节点的属性。

在 createWorkInProgress 函数中,调用了 createFiber() 函数构建一个新的fiber节点,我们来看看 createFiber。

createFiber

// react-reconciler/src/ReactFiber.new.js

const createFiber = function (
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
): Fiber {
  // 返回 fiberNode 节点的实例
  return new FiberNode(tag, pendingProps, key, mode);
};

在 createFiber 中,返回了FiberNode的一个实例对象,即返回一个新的fiber节点。下面贴出 FiberNode 构造函数的源码:

//react-reconciler/src/ReactFiber.new.js

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;

  if (enableProfilerTimer) {
    // Note: The following is done to avoid a v8 performance cliff.
    //
    // Initializing the fields below to smis and later updating them with
    // double values will cause Fibers to end up having separate shapes.
    // This behavior/bug has something to do with Object.preventExtension().
    // Fortunately this only impacts DEV builds.
    // Unfortunately it makes React unusably slow for some applications.
    // To work around this, initialize the fields below with doubles.
    //
    // Learn more about this here:
    // https://github.com/facebook/react/issues/14365
    // https://bugs.chromium.org/p/v8/issues/detail?id=8538
    this.actualDuration = Number.NaN;
    this.actualStartTime = Number.NaN;
    this.selfBaseDuration = Number.NaN;
    this.treeBaseDuration = Number.NaN;

    // It's okay to replace the initial doubles with smis after initialization.
    // This won't trigger the performance cliff mentioned above,
    // and it simplifies other profiler code (including DevTools).
    this.actualDuration = 0;
    this.actualStartTime = -1;
    this.selfBaseDuration = 0;
    this.treeBaseDuration = 0;
  }

  // ...
}

总结

本文介绍了 useFiber() 这个函数,它的作用是克隆旧节点,从而复用旧节点。在克隆旧节点时,是通过 双缓冲 的方式来复用旧节点。