关于javascript:ReactFiber节点的更新入口beginWork

2次阅读

共计 4087 个字符,预计需要花费 11 分钟才能阅读完成。

React 的更新工作次要是调用一个叫做 workLoop 的工作循环去构建 workInProgress 树,构建过程分为两个阶段:向下遍历和向上回溯,向下和向上的过程中会对路径的每个节点进行 beginWork 和 completeWork。

本文行将提到的 beginWork 是解决节点更新的入口,它会根据 fiber 节点的类型去调用不同的处理函数。

React 对每个节点进行 beginWork 操作,进入 beginWork 后,首先判断节点及其子树是否有更新,若有更新,则会在计算新状态和 diff 之后生成新的 Fiber,而后在新的 fiber 上标记 flags(effectTag),最初 return 它的子节点,以便持续针对子节点进行 beginWork。若它没有子节点,则返回 null,这样阐明这个节点是末端节点,能够进行向上回溯,进入 completeWork 阶段。

点击进入 React 源码调试仓库。

beginWork 的工作流程如下图,图中简化了流程,只对 App 节点进行了 beginWork 解决,其余节点流程类似

职责

通过概述可知 beginWork 阶段的整体工作是去更新节点,并返回子树,但真正的 beginWork 函数只是节点更新的入口,不会间接进行更新操作。作为入口,它的职责很显著,拦挡无需更新的节点。同时,它还会将 context 信息入到栈中(beginWork 入栈,completeWork 出栈),临时先不关注。

function beginWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderLanes: Lanes
): Fiber | null {
  // 获取 workInProgress.lanes,可通过判断它是否为空去判断该节点是否须要更新
  const updateLanes = workInProgress.lanes;

  // 根据 current 是否存在判断以后是首次挂载还是后续的更新
  // 如果是更新,先看优先级够不够,不够的话就能调用 bailoutOnAlreadyFinishedWork
  // 复用 fiber 节点来跳出对以后这个节点的解决了。if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (
        oldProps !== newProps ||
        hasLegacyContextChanged()) {didReceiveUpdate = true;} else if (!includesSomeLane(renderLanes, updateLanes)) {
      // 此时无需更新
      didReceiveUpdate = false;
      switch (workInProgress.tag) {
        case HostRoot:
          ...
        case HostComponent:
          ...
        case ClassComponent:
          ...
        case HostPortal:
          ...
      }

      // 拦挡无需更新的节点
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  } else {didReceiveUpdate = false;}

  // 代码走到这里阐明的确要去解决节点了,此时会依据不同 fiber 的类型
  // 去调用它们各自的处理函数

  // 先清空 workInProgress 节点上的 lanes,因为更新过程中用不到,// 在解决完 updateQueue 之后会从新赋值
  workInProgress.lanes = NoLanes;

  // 根据不同的节点类型来解决节点的更新
  switch (workInProgress.tag) {
    case IndeterminateComponent:
      ...
    case LazyComponent:
      ...
    case FunctionComponent:
      ...
      return updateFunctionComponent(
          current,
          workInProgress,
          Component,
          resolvedProps,
          renderLanes,
      );
    }
    case ClassComponent:
      ...
      return updateClassComponent(
          current,
          workInProgress,
          Component,
          resolvedProps,
          renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);

    ......
  }
}

能够看出,一旦节点进入 beginWork,会先去辨认该节点是否须要解决,若无需解决,则调用 bailoutOnAlreadyFinishedWork 复用节点,否则才真正去更新。

如何辨别更新与初始化过程

判断 current 是否存在。

这首先要了解 current 是什么,基于双缓冲的规定,调度更新时有两棵树,展现在屏幕上的 current Tree 和正在后盾基于 current 树构建的
workInProgress Tree。那么,current 和 workInProgress 能够了解为镜像的关系。workLoop 循环以后遍历到的 workInProgress 节点来自于它对应的 current 节点父级 fiber 的子节点(即 current 节点),所以 workInProgress 节点和 current 节点也是镜像的关系。

如果是首次渲染,对具体的 workInProgress 节点来说,它是没有 current 节点的,如果是在更新过程,因为 current 节点曾经在首次渲染时产生了,所以 workInProgress 节点有对应的 current 节点存在。

最终会依据节点是首次渲染还是更新来决定是创立 fiber 还是 diff fiber。只不过更新时,如果节点的优先级不够会间接复用已有节点,即走跳出(bailout)的逻辑,而不是去走上面的更新逻辑。

复用节点过程

节点可复用示意它无需更新。在下面 beginWork 的代码中能够看到,若节点的优先级不满足要求,阐明它不必更新,会调用 bailoutOnAlreadyFinishedWork 函数,去复用 current 节点作为新的 workInProgress 树的节点。

beginWork 函数中拦挡无需更新节点的逻辑

if (!includesSomeLane(renderLanes, updateLanes)) {
  ...

  // 此时无需更新,拦挡无需更新的节点
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

beginWork 它的返回值有两种状况:

  • 返回以后节点的子节点,而后会以该子节点作为下一个工作单元持续 beginWork,一直往下生成 fiber 节点,构建 workInProgress 树。
  • 返回 null,以后 fiber 子树的遍历就此终止,从以后 fiber 节点开始往回进行 completeWork。

bailoutOnAlreadyFinishedWork函数的返回值也是如此。

  • 返回以后节点的子节点,前置条件是以后节点的子节点有更新,此时以后节点未经解决,是能够间接复用的,复用的过程就是复制一份 current 节点的子节点,并把它 return 进来。
  • 返回 null,前提是以后子节点没有更新,以后子树的遍历过程就此终止。开始 completeWork。

从这个函数中,咱们也能够意识到,辨认以后 fiber 节点的子树有无更新显得尤为重要,这能够决定是否终止以后 Fiber 子树的遍历,将复杂度间接升高。实际上能够通过 fiber.childLanes 去辨认,childLanes 如果不为空,表明子树中有须要更新的节点,那么须要持续往下走。

标记 fiber.childLanes 的过程是在开始调度时产生的,在 markUpdateLaneFromFiberToRoot 函数中

带着上边的认知,来看一下源码理解具体的复用过程:

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {if (current !== null) {workInProgress.dependencies = current.dependencies;}

  // 标记有跳过的更新
  markSkippedUpdateLanes(workInProgress.lanes);

  // 如果子节点没有更新,返回 null,终止遍历
  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {return null;} else {
    // 子节点有更新,那么从 current 上复制子节点,并 return 进来
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}

总结

beginWork 的次要性能就是解决以后遍历到的 fiber,通过一番解决之后返回它的子 fiber,一个一个地往外吐出 fiber 节点,那么 workInProgress 树也就会被一点一点地构建进去。

这是 beginWork 的大抵流程,但实际上,外围更新的工作都是在各个更新函数中,这些函数会安顿 fiber 节点顺次进入两大解决流程:计算新状态和 Diff 算法,限于篇幅,这两个内容会分两篇文章具体解说,能够继续关注。

欢送扫码关注公众号,发现更多技术文章

正文完
 0