likes
comments
collection
share

React Fiber树的构建

作者站长头像
站长
· 阅读数 58

前面我们介绍了 React架构,为了实现可中断的异步更新v16 使用 Fiber 作为核心架构进行了重构,

接下来我们将深入 Fiber 树的构建,正式开始之前,我们先回顾下 Fiber 相关的一些术语,

  • Fiber 架构下,React 架构分为三层 SchedulerReconcilerRenderer
  • Reconciler 工作的阶段被称为 render 阶段,在该阶段会调用组件的 render 方法
  • Renderer 工作的阶段被称为 commit 阶段,在阶段会把 render 阶段提交的信息渲染到页面
  • rendercommit 阶段统称为 work ,如果任务正在 Scheduler 内调度,则不属于work

Fiber 树的构建发生在 render 阶段,也就是 Reconciler 工作的阶段,我们知道 Fiber Reconciler 是由 Stack Reconciler 重构而来,通过遍历的方式实现可中断的递归,所以 Fiber Reconciler 的工作可以分为两个部分:“递”“归”

可中断的递和归

render 阶段开始于 performSyncWorkOnRoot performConcurrentWorkOnRoot 方法,

分别会调用 workLoopSyncworkLoopConcurrent,这取决于本次更新是同步更新还是异步更新

// 如果是同步更新,会调用 performSyncWorkOnRoot,performSyncWorkOnRoot 会调用 workLoopSync
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// 如果是异步更新,会调用 performConcurrentWorkOnRoot,performConcurrentWorkOnRoot 会调用 workLoopConcurrent
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

可以看到,它们都会调用 performUnitOfWork ,唯一的区别是是否判断 shouldYield

如果浏览器没有空闲时间,shouldYield 会终止循环,直到浏览器有空闲时间后再继续遍历。

performUnitOfWork 就是递和归的起点,参数 workInProgress 就是我们之前介绍的正在构建中的 Fiber 树,

递归遍历的流程为,

  1. 首先进入“递”阶段,从 rootFiber 开始向下进行深度优先遍历,为遍历到的每个节点调用 beginWork 方法
  2. beginWork 会根据传入的 Fiber 节点创建子 Fiber 节点,并将这两个 Fiber 节点连接起来
  3. 遍历到叶子节点时进入“归”阶段
  4. 归”阶段会为每个节点调用 completeWork
  5. 如果节点存在兄弟(sibling)节点,进入兄弟节点的“递”阶段,如果不存在,进入父节点的“归”阶段
  6. 递”和“归”阶段会交错执行直到“归”到 rootFiber

为了方便理解,我们以下面的代码为例,

function App() {
  return (
    <div>
      Monch
      <span>Lee</span>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root"));

对应的 Fiber 树结构如下图,我们把节点的 beginWorkcompleteWork 打印出来,

React Fiber树的构建

render 阶段会依次执行,

React Fiber树的构建
1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. "Monch" Fiber beginWork
5. "Monch" Fiber completeWork
6. span Fiber beginWork
7. span Fiber completeWork
8. div Fiber completeWork
9. App Fiber completeWork
10. rootFiber completeWork

你可能会疑惑,为什么没有 “Lee” Fiber?

原因是作为一种性能优化手段,React 会特殊处理只有单一文本子节点Fiber

render 阶段的递归本质上就是 beginWorkcompleteWork,我们先从 beginWork 开始。

beginWork

函数 beginWork 的定义如下,你可以从源码的这里找到完整的定义,

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...
}

前面我们介绍,beginWork 的工作是:传入当前 Fiber 节点,创建子 Fiber 节点

整体流程如下,

React Fiber树的构建

beginWork 接受三个参数,其中,

  • current,当前组件对应的 Fiber 节点在上一次更新时的 Fiber 节点,即 workInProgress.alternate
  • workInProgress,当前组件对应的 Fiber 节点
  • renderLanes,当前 Fiber 节点的优先级

深入函数体之前,先思考一个问题🤔 ,对于一个 Fiber 节点而言,可能存在首次渲染更新两种情况,我们要怎么区分呢?

React Fiber架构 中我们介绍到,Fiber 架构是基于双缓存技术,React 中最多同时存在两颗 Fiber 树,除了 rootFiber 外,首次渲染 mount 时,是不存在当前 Fiber 节点在上一次更新时的 Fiber 节点的,

所以我们可以通过判断当前的 current Fiber 树是否为 null 来决定是 mount 还是 update

如果 current !== null,说明是 update,React 会尝试复用节点,否则会创建节点,

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  // 如果 current 存在,说明是 update,此时可能存在优化路径,React 会尝试复用 current(即上一次更新的 Fiber 节点)
  if (current !== null) {
    // 尝试复用 current
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  } else {
    // 如果 current 不存在,说明是 mount,此时会创建子 Fiber 节点
    didReceiveUpdate = false;
  }

  // mount时,根据 tag 不同,创建不同的子 Fiber 节点
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    // ...
    case FunctionComponent:
    // ...
    case ClassComponent:
    // ...
    case HostComponent:
    // ...
  }
}

什么情况下可以复用节点呢?

节点在满足 didReceiveUpdate === false 时 React 会尝试复用,

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;
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  } else {
    didReceiveUpdate = false;
  }
} else {
  didReceiveUpdate = false;
}

也就是需要满足以下情况,

// props 相同,节点类型相同,节点的优先级不够
oldProps === newProps && workInProgress.type === current.type && !includesSomeLane(renderLanes, updateLanes)

这与我们前面介绍的 O(n) 的启发式 Diffing 算法的假设是相呼应的,只有类型相同的元素会继续 Diffing

不满足复用条件时,React 会根据节点类型(tag)调用对应的方法创建子节点,最终都会调用 reconcileChildren

reconcileChildren

reconcileChildren 会创建新的 Fiber 节点,与 beginWork 类似,通过 current === null 区分是 mount 还是 update

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes
    );
  }
}
  1. 如果是 mount,创建新的子 Fiber 节点
  2. 如果是 update,调用 Diffing 算法,将比较的结果生成新 Fiber 节点

不论走哪个逻辑,最终都会生成新的子 Fiber 节点并赋值给 workInProgress.child

这个值会作为本次 beginWork返回值,及下次 performUnitOfWork 执行时 workInProgress传参

mountChildFibersreconcileChildFibers 逻辑基本一致,唯一的区别是后者会为生成的 Fiber 带上 effectTag 属性,

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

function ChildReconciler(shouldTrackSideEffects) {
  // ...
}

什么是 effectTag 呢?

effectTag

我们知道,render 阶段是在内存中进行的,结束后会通知 Renderer 需要执行的 DOM 操作,

这里的 effectTag 就是要执行 DOM 操作的具体类型

export const Placement = /*                */ 0b00000000000010; // 插入
export const Update = /*                   */ 0b00000000000100; // 更新
export const PlacementAndUpdate = /*       */ 0b00000000000110; // 插入并更新
export const Deletion = /*                 */ 0b00000000001000; // 删除
// ...

这里通过二进制表示 effectTag,是为了方便的使用位操作为 fiber.effectTag 赋值多个 effect

前面我们提到,mountChildFibers 不会为 Fiber 节点赋值 effectTag那么首屏渲染是如何完成呢?

实际上,mount 时只会对 rootFiber 赋值 Placement effectTag

这样可以保证在 commit 阶段只需要一次 DOM 插入就完成整个 DOM 树的首屏渲染,

function ChildReconciler(shouldTrackSideEffects) {
  // ...

  // reconcileChildFibers 会调用 placeChild 等方法为递归到的 Fiber 节点打上 effectTag
  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number
  ): number {
    if (current !== null) {
      // ...
    } else {
      newFiber.effectTag = Placement;
      return lastPlacedIndex;
    }
  }

  // mountChildFibers 只会为 rootFiber 打上一个 Placement effectTag,作为一种优化手段,在 commit 阶段一次性完成首屏渲染
  function placeSingleChild(newFiber: Fiber): Fiber {
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.effectTag = Placement;
    }
    return newFiber;
  }
}

要插入 effectTag,需要 Fiber 节点中保存对应的 DOM 节点,也就是 stateNode 属性,它会在 completeWork 中创建。

completeWork

类似于 beginWorkcompleteWork 也是针对不同的 fiber.tag 调用不同的处理逻辑,

React Fiber树的构建

源码 completeWork 的定义如下,

function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case FunctionComponent:
    case Fragment:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...
      return null;
    }
    case HostRoot: {
      // ...
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...
      return null;
    }
  }
}

我们以页面渲染必须的 HostComponent 为例,看下 completeWork 都做了哪些工作。

HostComponent

beginWork 类似,可以通过 current === null? 判断当前是处于 mount 还是 update 流程,

由于 update 时依赖真实的 DOM 节点,所以还要考虑 workInProgress.stateNode != null ?

case HostComponent: {
  popHostContext(workInProgress);
  const rootContainerInstance = getRootHostContainer();
  const type = workInProgress.type;

  // 如果 current 存在并且 Fiber 节点存在对应的 DOM 节点
  if (current !== null && workInProgress.stateNode != null) {
    // update
  } else {
    // mount
  }
  return null;
}

mount 中存在和 update 类似的流程,这里我们先看 completeWorkupdate 流程。

update

根据前面的判断条件,update 时,Fiber 节点已经存在 DOM 节点,所以不需要创建 DOM,

update 会调用 updateHostComponent 来处理 props,包括,

updateHostComponent = function (
  current: Fiber,
  workInProgress: Fiber,
  type: Type,
  newProps: Props,
  rootContainerInstance: Container
) {
  const oldProps = current.memoizedProps;
  if (oldProps === newProps) {
    return;
  }

  const instance: Instance = workInProgress.stateNode;
  const currentHostContext = getHostContext();
  const updatePayload = prepareUpdate(
    instance,
    type,
    oldProps,
    newProps,
    rootContainerInstance,
    currentHostContext
  );

  // 处理完的 props 会被赋值给 workInProgress.updateQueue,最终会在 commit 阶段被渲染在页面上
  // updatePayload 为数组形式,偶数索引的值为变化的 prop key,奇数索引的值为变化的 prop value
  workInProgress.updateQueue = (updatePayload: any);
  if (updatePayload) {
    markUpdate(workInProgress);
  }
};
  1. onClickonChange 等回调函数的注册
  2. 处理 style prop
  3. 处理 DANGEROUSLY_SET_INNER_HTML prop
  4. 处理 children prop

如果是 mount ,流程会有所不同。

mount

mount 流程的主要逻辑包含三个,

  1. Fiber 节点生成对应的 DOM 节点
  2. 将子孙 DOM 节点插入刚生成的 DOM 节点中
  3. update 阶段的 updateHostComponent 类似,处理 props
const currentHostContext = getHostContext();
// 为 Fiber 创建对应 DOM 节点
const instance = createInstance(
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
    workInProgress,
  );
// 将子孙 DOM 节点插入刚生成的 DOM 节点中
appendAllChildren(instance, workInProgress, false, false);
// DOM 节点赋值给 fiber.stateNode
workInProgress.stateNode = instance;

// 处理 props
if (
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress);
}

这里的 appendAllChildren 就是我们前面提到的离屏 DOM 树创建的关键,每次调用 appendAllChildren 都会将已生成的子孙 DOM 节点插入当前生成的 DOM 节点下,当递归到 rootFiber 时,我们就可以得到一个构建好的离屏 DOM 树,这样 commit 阶段就可以只通过一次插入 DOM 的操作将整棵 DOM 树插入到页面。

最后,所有存在 effectTagFiber 会生成 effectList

effectList

effectList 是一个单向链表,在 completeWork 的上层函数 completeUnitOfWork 中构造,每个执行完 completeWork 且存在 effectTagFiber 节点会被保存链表中,为什么需要缓存 effectTag 呢?

这里的原因也是为了提效。

作为 DOM 操作的依据,commit 阶段需要找到所有有 effectTagFiber 节点并依次执行 effectTag 对应操作,如果不缓存,则需要再遍历一次 Fiber 树寻找 effectTag !== null 的节点,所以为了避免这种低效的操作,所有有 effectTag 的Fiber节点都会被追加在 effectList 中,最终形成一条以 rootFiber.firstEffect 为起点的单向链表,

React Fiber树的构建

这样,在 commit 阶段只需要遍历 effectList 就能执行所有 effect

小结

completeWork 的工作是对上一个节点 Diffing 完成后进行一些收尾工作,会根据不同的 tag 执行 mountupdate 操作,

如果节点需要 mount,会为其创建对应的 DOM 节点并赋值给 fiber.stateNode,这个过程会将子孙 DOM 节点依次插入生成离屏的 DOM 树,同时还会初始化 DOM 对象的事件监听器及内部属性。

如果节点需要 update,会 diff props,返回一个需要更新的属性名组成的数组然后赋值给 workInProgress.updateQueue

最后有 effectTagfiber 会生成一个单向链表 effectList 挂载到父级 fiber,并返回下一个 workInProgress

render 阶段工作完成后,fiberRootNode 会被传递给 commitRoot方法,开启 commit 阶段的工作流程。

commitRoot(root);

参考链接

写在最后

本文首发于我的 博客,才疏学浅,难免有错误,文章有误之处还望不吝指正!

如果有疑问或者发现错误,可以在评论区进行提问和勘误,

如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

转载自:https://juejin.cn/post/7195456123183300669
评论
请登录