ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

React源码解析————Render阶段(三)

2021-12-11 22:59:37  阅读:153  来源: 互联网

标签:Render DOM work 节点 React returnFiber 源码 null workInProgress


React源码解析————Render阶段(三)

2021SC@SDUSC

2021SC@SDUSC

CompleteWork

流程预览

类似beginWork,completeWork也是针对不同fiber.tag调用不同的处理逻辑。

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

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略

我们重点关注页面渲染所必须的HostComponent(即原生DOM组件对应的Fiber节点),其他类型Fiber的处理留在具体功能实现时讲解。

HostComponent

和beginWork一样,我们根据current === null ?判断是mount还是update。
同时针对HostComponent,判断update时我们还需要考虑workInProgress.stateNode != null ?(即该Fiber节点是否存在对应的DOM节点)

    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );

        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
        if (!newProps) {
          invariant(
            workInProgress.stateNode !== null,
            'We must have new props for new mounts. This error is likely ' +
              'caused by a bug in React. Please file an issue.',
          );
          // This can happen when we abort work.
          bubbleProperties(workInProgress);
          return null;
        }

        const currentHostContext = getHostContext();
        // TODO: Move createInstance to beginWork and keep it on a context
        // "stack" as the parent. Then append children as we go in beginWork
        // or completeWork depending on whether we want to add them top->down or
        // bottom->up. Top->down is faster in IE11.
        const wasHydrated = popHydrationState(workInProgress);
        if (wasHydrated) {
          // TODO: Move this and createInstance step into the beginPhase
          // to consolidate.
          if (
            prepareToHydrateHostInstance(
              workInProgress,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            // If changes to the hydrated node need to be applied at the
            // commit-phase we mark this as such.
            markUpdate(workInProgress);
          }
        } else {
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );

          appendAllChildren(instance, workInProgress, false, false);

          workInProgress.stateNode = instance;

          // Certain renderers require commit-time effects for initial mount.
          // (eg DOM renderer supports auto-focus for certain elements).
          // Make sure such renderers get scheduled for later work.
          if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
        }

        if (workInProgress.ref !== null) {
          // If there is a ref on a host node we need to schedule a callback
          markRef(workInProgress);
        }
      }
      bubbleProperties(workInProgress);
      return null;
    }

当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:
1.onClick、onChange等回调函数的注册
2.处理style prop
3.处理DANGEROUSLY_SET_INNER_HTML prop
4.处理children prop
我们去掉一些当前不需要关注的功能(比如ref)。可以看到最主要的逻辑是调用updateHostComponent方法。

if (current !== null && workInProgress.stateNode != null) {
  // update的情况
  updateHostComponent(
    current,
    workInProgress,
    type,
    newProps,
    rootContainerInstance,
  );
}

我们可以从这里看到updateHostComponent方法定义。

在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。

  updateHostComponent = function(
    current: Fiber,
    workInProgress: Fiber,
    type: Type,
    newProps: Props,
    rootContainerInstance: Container,
  ) {
    // If we have an alternate, that means this is an update and we need to
    // schedule a side-effect to do the updates.
    const oldProps = current.memoizedProps;
    if (oldProps === newProps) {
      // In mutation mode, this is sufficient for a bailout because
      // we won't touch this node even if children changed.
      return;
    }

    // If we get updated because one of our children updated, we don't
    // have newProps so we'll have to reuse them.
    // TODO: Split the update API as separate for the props vs. children.
    // Even better would be if children weren't special cased at all tho.
    const instance: Instance = workInProgress.stateNode;
    const currentHostContext = getHostContext();
    // TODO: Experiencing an error where oldProps is null. Suggests a host
    // component is hitting the resume path. Figure out why. Possibly
    // related to `hidden`.
    const updatePayload = prepareUpdate(
      instance,
      type,
      oldProps,
      newProps,
      rootContainerInstance,
      currentHostContext,
    );
    // TODO: Type this specific to this type of component.
    workInProgress.updateQueue = (updatePayload: any);
    // If the update payload indicates that there is a change or if there
    // is a new ref we mark this as an update. All the work is done in commitWork.
    if (updatePayload) {
      markUpdate(workInProgress);
    }
  };

其中updatePayload为数组形式,他的奇数索引的值为变化的prop key,偶数索引的值为变化的prop value。
当mount时,同样,我们省略了不相关的逻辑。可以看到,mount时的主要逻辑包括三个:
1.为Fiber节点生成对应的DOM节点
2.将子孙DOM节点插入刚生成的DOM节点中
3.与update逻辑中的updateHostComponent类似的处理props的过程

// mount的情况

// ...省略服务端渲染相关逻辑

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;

// 与update逻辑中的updateHostComponent类似的处理props的过程
if (
  finalizeInitialChildren(
    instance,
    type,
    newProps,
    rootContainerInstance,
    currentHostContext,
  )
) {
  markUpdate(workInProgress);
}

还记得我们讲到:mount时只会在rootFiber存在Placement effectTag。那么commit阶段是如何通过一次插入DOM操作(对应一个Placement effectTag)将整棵DOM树插入页面的呢?
原因就在于completeWork中的appendAllChildren方法。
由于completeWork属于“归”阶段调用的函数,每次调用appendAllChildren时都会将已生成的子孙DOM节点插入当前生成的DOM节点下。那么当“归”到rootFiber时,我们已经有一个构建好的离屏DOM树。

completeUnitOfWork

至此render阶段的绝大部分工作就完成了。

但是还有一个问题:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。难道需要在commit阶段再遍历一次Fiber树寻找effectTag !== null的Fiber节点么?
这显然是很低效的。

为了解决这个问题,在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点会被保存在一条被称为effectList的单向链表中。

effectList中第一个Fiber节点保存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect。

//完成当前节点的 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;
}

该代码的作用是完成当前节点的work,并赋值Effect链,然后移动到兄弟节点,重复该操作,当没有更多兄弟节点时,返回至父节点,最终返回至root节点
整体上看是一个大的while循环:
从当前节点开始,遍历到兄弟节点,当无兄弟节点时,返回至父节点,
再从父节点开始,遍历到兄弟节点,当无兄弟节点时,返回至父父节点,
可想而知,最终会返回至rootFiber节点
EffectList的赋值:
假设Span1有更新,Span2也有更新
在这里插入图片描述
那么父节点DIV的firstEffect和lastEffect在Span1执行completeUnitOfWork()后,会是下面这个样子:
在这里插入图片描述
workInProgress1即表示Span1对应的fiber对象

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

在这里插入图片描述也就是说:Effect链是帮助父节点简单判断子节点是否有更新及更新顺序的。

小结

至此,render阶段全部工作完成。在performSyncWorkOnRoot函数中fiberRootNode被传递给commitRoot方法,开启commit阶段工作流程。

流程图来源于网络,作者看到请和我联系。
在这里插入图片描述

标签:Render,DOM,work,节点,React,returnFiber,源码,null,workInProgress
来源: https://blog.csdn.net/m0_51744158/article/details/121658798

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有