专栏首页前端干货和生活感悟React源码解析之completeUnitOfWork

React源码解析之completeUnitOfWork

前言:

(1) 关于completeUnitOfWork()在哪里使用到,请看下 React源码解析之workLoop 中的二、performUnitOfWork

(2) 本文需要了解的基础知识

workInProgressfiber对象,它的相关属性请看:React源码解析之RootFiber

② 源码里A&B===C&运算符的含义,请看 前端小知识10点 中的第八点

一、completeUnitOfWork

作用: 完成当前节点的work,并赋值Effect链,然后移动到兄弟节点,重复该操作,当没有更多兄弟节点时,返回至父节点,最终返回至root节点

源码:

//完成当前节点的 work,然后移动到兄弟节点,重复该操作,当没有更多兄弟节点时,返回至父节点
function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
  // Attempt to complete the current unit of work, then move to the next
  // sibling. If there are no more siblings, return to the parent fiber.

  //从下至上,移动到该节点的兄弟节点,如果一直往上没有兄弟节点,就返回父节点
  //可想而知,最终会到达 root 节点
  workInProgress = unitOfWork;
  do {
    // The current, flushed, state of this fiber is the alternate. Ideally
    // nothing should rely on this, but relying on it here means that we don't
    // need an additional field on the work in progress.

    //获取当前节点
    const current = workInProgress.alternate;
    //获取父节点
    const returnFiber = workInProgress.return;

    // Check if the work completed or if something threw.
    //判断节点的操作是否完成,还是有异常丢出
    //Incomplete表示捕获到该节点抛出的 error

    //&是表示位的与运算,把左右两边的数字转化为二进制,然后每一位分别进行比较,如果相等就为1,不相等即为0

    //如果该节点没有异常抛出的话,即可正常执行
    if ((workInProgress.effectTag & Incomplete) === NoEffect) {
      //dev 环境,可不看
      setCurrentDebugFiberInDEV(workInProgress);

      let next;
      //如果不能使用分析器的 timer 的话,直接执行completeWork,
      //否则执行分析器timer,并执行completeWork
      if (
        !enableProfilerTimer ||
        (workInProgress.mode & ProfileMode) === NoMode
      ) {
        //完成该节点的更新
        next = completeWork(current, workInProgress, renderExpirationTime);
      } else {
        //启动分析器的定时器,并赋成当前时间
        startProfilerTimer(workInProgress);
        //完成该节点的更新
        next = completeWork(current, workInProgress, renderExpirationTime);
        // Update render duration assuming we didn't error.
        //在没有报错的前提下,更新渲染持续时间

        //记录分析器的timer的运行时间间隔,并停止timer
        stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);
      }
      //停止 work 计时,可不看
      stopWorkTimer(workInProgress);
      //dev 环境,可不看
      resetCurrentDebugFiberInDEV();
      //更新该节点的 work 时长和子节点的 expirationTime
      resetChildExpirationTime(workInProgress);
      //如果next存在,则表示产生了新 work
      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        //返回 next,以便执行新 work
        return next;
      }
      //如果父节点存在,并且其 Effect 链没有被赋值的话
      if (
        returnFiber !== null &&
        // Do not append effects to parents if a sibling failed to complete
        (returnFiber.effectTag & Incomplete) === NoEffect
      ) {
        // Append all the effects of the subtree and this fiber onto the effect
        // list of the parent. The completion order of the children affects the
        // side-effect order.
        //子节点的完成顺序会影响副作用的顺序

        //如果父节点没有挂载firstEffect的话,将当前节点的firstEffect赋值给父节点的firstEffect
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = workInProgress.firstEffect;
        }
        //同上,根据当前节点的lastEffect,初始化父节点的lastEffect
        if (workInProgress.lastEffect !== null) {
          //如果父节点的lastEffect有值的话,将nextEffect赋值
          //目的是串联Effect链
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
          }
          returnFiber.lastEffect = workInProgress.lastEffect;
        }

        // If this fiber had side-effects, we append it AFTER the children's
        // side-effects. We can perform certain side-effects earlier if needed,
        // by doing multiple passes over the effect list. We don't want to
        // schedule our own side-effect on our own list because if end up
        // reusing children we'll schedule this effect onto itself since we're
        // at the end.
        //获取副作用标记
        const effectTag = workInProgress.effectTag;

        // Skip both NoWork and PerformedWork tags when creating the effect
        // list. PerformedWork effect is read by React DevTools but shouldn't be
        // committed.
        //如果该副作用标记大于PerformedWork
        if (effectTag > PerformedWork) {
          //当父节点的lastEffect不为空的时候,将当前节点挂载到父节点的副作用链的最后
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = workInProgress;
          } else {
            //否则,将当前节点挂载在父节点的副作用链的头-firstEffect上
            returnFiber.firstEffect = workInProgress;
          }
          //无论父节点的lastEffect是否为空,都将当前节点挂载在父节点的副作用链的lastEffect上
          returnFiber.lastEffect = workInProgress;
        }
      }
    }
    //如果该 fiber 节点未能完成 work 的话(报错)
    else {
      // This fiber did not complete because something threw. Pop values off
      // the stack without entering the complete phase. If this is a boundary,
      // capture values if possible.
      //节点未能完成更新,捕获其中的错误
      const next = unwindWork(workInProgress, renderExpirationTime);

      // Because this fiber did not complete, don't reset its expiration time.
      //由于该 fiber 未能完成,所以不必重置它的 expirationTime
      if (
        enableProfilerTimer &&
        (workInProgress.mode & ProfileMode) !== NoMode
      ) {
        // Record the render duration for the fiber that errored.
        //记录分析器的timer的运行时间间隔,并停止timer
        stopProfilerTimerIfRunningAndRecordDelta(workInProgress, false);

        // Include the time spent working on failed children before continuing.
        //虽然报错了,但仍然会累计 work 时长
        let actualDuration = workInProgress.actualDuration;
        let child = workInProgress.child;
        while (child !== null) {
          actualDuration += child.actualDuration;
          child = child.sibling;
        }
        workInProgress.actualDuration = actualDuration;
      }
      //如果next存在,则表示产生了新 work
      if (next !== null) {
        // If completing this work spawned new work, do that next. We'll come
        // back here again.
        // Since we're restarting, remove anything that is not a host effect
        // from the effect tag.
        // TODO: The name stopFailedWorkTimer is misleading because Suspense
        // also captures and restarts.
        //停止失败的 work 计时,可不看
        stopFailedWorkTimer(workInProgress);
        //更新其 effectTag,标记是 restart 的
        next.effectTag &= HostEffectMask;
        //返回 next,以便执行新 work
        return next;
      }
      //停止 work 计时,可不看
      stopWorkTimer(workInProgress);
      //如果父节点存在的话,重置它的 Effect 链,标记为「未完成」
      if (returnFiber !== null) {
        // Mark the parent fiber as incomplete and clear its effect list.
        returnFiber.firstEffect = returnFiber.lastEffect = null;
        returnFiber.effectTag |= Incomplete;
      }
    }
    //获取兄弟节点
    const siblingFiber = workInProgress.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      return siblingFiber;
    }
    // Otherwise, return to the parent
    //如果能执行到这一步的话,说明 siblingFiber 为 null,
    //那么就返回至父节点
    workInProgress = returnFiber;
  } while (workInProgress !== null);

  // We've reached the root.
  //当执行到这里的时候,说明遍历到了 root 节点,已完成遍历
  //更新workInProgressRootExitStatus的状态为「已完成」
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
  return null;
}

解析: ① 整体上看是一个大的while循环: 从当前节点开始,遍历到兄弟节点,当无兄弟节点时,返回至父节点, 再从父节点开始,遍历到兄弟节点,当无兄弟节点时,返回至父父节点, 可想而知,最终会返回至rootFiber节点

② 当整颗 fiber 树遍历完成后,更新workInProgressRootExitStatus的状态为「已完成」

我们来看一下do...while内部的逻辑: (1) 如果该节点可正常执行的话

① 直接执行completeWork()方法,更新该节点(从fiber对象转变成真实的DOM节点)

ps:下篇解析该方法

② 如果可以启用ProfilerTimer的话,则执行startProfilerTimer()stopProfilerTimerIfRunningAndRecordDelta(),用来记录fiber节点执行work的实际开始时间(actualStartTime)和work时长(actualDuration)

详细解析请看本文的「二、startProfilerTimer和stopProfilerTimerIfRunningAndRecordDelta」

stopWorkTimer()的作用是停止work计时,不是很重要,可不看

resetChildExpirationTime的作用是更新该节点的work时长和获取优先级最高的子节点的expirationTime

详细解析请看本文的「三、resetChildExpirationTime」

⑤ 如果next存在,则表示该节点在这次更新完成后,产生了新的更新,那么就返回该next,并将其作为completeUnitOfWork()的参数,再次执行

⑥ 接下来这一段比较重要,是 Effect 链的赋值,看个例子:假设Span1有更新,Span2也有更新

那么父节点DIVfirstEffectlastEffectSpan1执行completeUnitOfWork()后,会是下面这个样子:

workInProgress1即表示Span1对应的fiber对象

当轮到Span2执行completeUnitOfWork()后,又会变成下面这个样子:

也就是说:Effect链是帮助父节点简单判断子节点是否有更新及更新顺序的

else的情况就是执行更新的过程中捕获到error的情况,此时执行的是unwindWork而不是completeWork,与completeWork最大的区别是有ShouldCapture的判断,也是后续文章会讲到

else后面的逻辑跟上面大同小异了,不再赘述

之后是遍历兄弟节点,返回父节点,再次遍历,不再赘述

⑧ 可以看到,completeUnitOfWork主要做了三件事: (1) 执行completeWork,完成节点更新 (2) 执行resetChildExpirationTime,获取优先级最高的childExpirationTime (3) 赋值Effect

二、startProfilerTimer和stopProfilerTimerIfRunningAndRecordDelta

作用: 记录fiber节点执行work的实际开始时间(actualStartTime)和work时长

源码: startProfilerTimer()

//启动分析器的timer,并赋成当前时间
function startProfilerTimer(fiber: Fiber): void {
  //如果不能启动分析器的timer的话,就 return
  if (!enableProfilerTimer) {
    return;
  }
  //分析器的开始时间
  profilerStartTime = now();
  //如果 fiber 节点的实际开始时间 < 0 的话,则赋成当前时间
  if (((fiber.actualStartTime: any): number) < 0) {
    fiber.actualStartTime = now();
  }
}

stopProfilerTimerIfRunningAndRecordDelta()

//记录分析器的timer的work 时间,并停止timer
function stopProfilerTimerIfRunningAndRecordDelta(
  fiber: Fiber,
  overrideBaseTime: boolean,
): void {
  //如果不能启动分析器的定时器的话,就 return
  if (!enableProfilerTimer) {
    return;
  }
  //如果分析器的开始时间>=0的话
  if (profilerStartTime >= 0) {
    //获取运行的时间间隔
    const elapsedTime = now() - profilerStartTime;
    //累计实际 work 时间间隔
    fiber.actualDuration += elapsedTime;
    if (overrideBaseTime) {
      //记录时间间隔
      fiber.selfBaseDuration = elapsedTime;
    }
    //上述操作完成后,将分析器的timer的开始时间重置为-1
    profilerStartTime = -1;
  }
}

解析: 逻辑比较简单,就不额外补充了

三、resetChildExpirationTime

作用: 更新该节点的work时长和获取优先级最高的子节点的expirationTime

源码:

//更新该节点的 work 时长和获取优先级最高的子节点的 expirationTime
function resetChildExpirationTime(completedWork: Fiber) {
  //如果当前渲染的节点需要更新,但是子节点不需要更新的话,则 return
  if (
    renderExpirationTime !== Never &&
    completedWork.childExpirationTime === Never
  ) {
    // The children of this component are hidden. Don't bubble their
    // expiration times.
    return;
  }

  let newChildExpirationTime = NoWork;

  // Bubble up the earliest expiration time.
  if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
    // In profiling mode, resetChildExpirationTime is also used to reset
    // profiler durations.
    //获取当前节点的实际 work 时长
    let actualDuration = completedWork.actualDuration;
    //获取 fiber 树的 work 时长
    let treeBaseDuration = completedWork.selfBaseDuration;

    // When a fiber is cloned, its actualDuration is reset to 0. This value will
    // only be updated if work is done on the fiber (i.e. it doesn't bailout).
    // When work is done, it should bubble to the parent's actualDuration. If
    // the fiber has not been cloned though, (meaning no work was done), then
    // this value will reflect the amount of time spent working on a previous
    // render. In that case it should not bubble. We determine whether it was
    // cloned by comparing the child pointer.
    // 当一个 fiber 节点被克隆后,它的实际 work 时长被重置为 0.
    // 这个值只会在 fiber 自身上的 work 完成时被更新(顺利执行的话)
    // 当 fiber 自身 work 完成后,将自身的实际 work 时长冒泡赋给父节点的实际 work 时长
    // 如果 fiber 没有被克隆,即 work 未被完成的话,actualDuration 反映的是上次渲染的实际 work 时长
    // 如果是这种情况的话,不应该冒泡赋给父节点
    // React 通过比较 子指针 来判断 fiber 是否被克隆

    // 关于 alternate 的作用,请看:https://juejin.im/post/5d5aa4695188257573635a0d
    // 是否将 work 时间冒泡至父节点的依据是:
    // (1) 该 fiber 节点是否是第一次渲染
    // (2) 该 fiber 节点的子节点有更新
    const shouldBubbleActualDurations =
      completedWork.alternate === null ||
      completedWork.child !== completedWork.alternate.child;

    //获取当前节点的第一个子节点
    let child = completedWork.child;

    //当该子节点存在时,通过newChildExpirationTime来获取子节点、子子节点两者中优先级最高的那个expirationTime
    while (child !== null) {
      //获取该子节点的 expirationTime
      const childUpdateExpirationTime = child.expirationTime;
      //获取该子节点的 child 的 expirationTime
      const childChildExpirationTime = child.childExpirationTime;
      //如果子节点的优先级大于NoWork的话,则将newChild的 expirationTime 赋值为该子节点的 expirationTime
      if (childUpdateExpirationTime > newChildExpirationTime) {
        newChildExpirationTime = childUpdateExpirationTime;
      }
      //子节点的 child 同上
      if (childChildExpirationTime > newChildExpirationTime) {
        newChildExpirationTime = childChildExpirationTime;
      }

      if (shouldBubbleActualDurations) {
        //累计子节点的 work 时长
        actualDuration += child.actualDuration;
      }
      //累计 fiber 树的 work 时长
      treeBaseDuration += child.treeBaseDuration;
      //移动到兄弟节点,重复上述过程
      child = child.sibling;
    }
    //更新 fiber 的 work 时长
    completedWork.actualDuration = actualDuration;
    //更新 fiber 树的 work 时长
    completedWork.treeBaseDuration = treeBaseDuration;
  }
  //逻辑同上,不再赘述
  else {
    let child = completedWork.child;
    while (child !== null) {
      const childUpdateExpirationTime = child.expirationTime;
      const childChildExpirationTime = child.childExpirationTime;
      if (childUpdateExpirationTime > newChildExpirationTime) {
        newChildExpirationTime = childUpdateExpirationTime;
      }
      if (childChildExpirationTime > newChildExpirationTime) {
        newChildExpirationTime = childChildExpirationTime;
      }
      child = child.sibling;
    }
  }

  completedWork.childExpirationTime = newChildExpirationTime;
}

解析: (1) 将累计的子节点的work时长冒泡赋值到父节点的actualDuration

(2) 循环遍历目标节点的子节点们,将子节点中优先级最高的expirationTime更新到目标及诶按的childExpirationTime

(3) 关于childExpirationTime的详细解释,请看: React之childExpirationTime

四、GitHub https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-reconciler/src/ReactFiberWorkLoop.js


小进进还没开通留言功能,觉得不错的话,点「在看」、转发朋友圈都是一种支持 (●'◡'●)ノ

本文分享自微信公众号 - webchen(webchen1995),作者:webchen

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-02-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • React源码解析之FunctionComponent(中)

    作用: 当子节点不为 null,则复用子节点并删除其兄弟节点; 当子节点为 null,则创建新的 fiber 节点

    进击的小进进
  • React源码解析之FunctionComponent(上)

    在 React源码解析之workLoop 中讲到当workInProgress.tag为FunctionComponent时,会进行FunctionCompon...

    进击的小进进
  • React源码解析之updateHostComponent和updateHostText

    还是在 React源码解析之workLoop 中,有一段HostComponent和HostText的更新:

    进击的小进进
  • React源码解析之FunctionComponent(中)

    作用: 当子节点不为 null,则复用子节点并删除其兄弟节点; 当子节点为 null,则创建新的 fiber 节点

    进击的小进进
  • 二叉查找树

    二叉查找树 (Binary Search Tree) 是按照平衡顺序排列的二叉树, 也称二叉搜索树、 有序二叉树(ordered binary tree),排序...

    beginor
  • BAT面试算法进阶(2)-两数相加

    You are given two non-empty linked lists representing two non-negative integ...

    CC老师
  • 你的简历写了 “熟悉” zookeeper ?那这些你会吗?

    Zookeeper 它作为Hadoop项目中的一个开源子项目,是一个经典的分布式数据一致性解决方案,致力于为分布式应用提供一个高性能、高可用,且具有严格顺序访问...

    程序员内点事
  • 答应我,不会这些概念,简历不要写 “熟悉” zookeeper

    本文主要分享一下zookeeper的一些基本概念,在正式进入正题前,和大家聊一聊刚入行时我的面试经验,可以说是耿直的有些可爱。

    程序员内点事
  • tomcat(一):一次解决tomcat9无法登陆控制界面的经历

        在进行Tomcat热部署的时候遇到一个问题,无法正常进入Tomcat的管理页面,进行了一次实验,在此与大家共享,或许会对读者有些许帮助。

    拓荒者
  • 资源 | 清华大学发布OpenNE:用于网络嵌入的开源工具包

    机器之心

扫码关注云+社区

领取腾讯云代金券