React源码解析之workLoop

·  阅读 2872

前言:
React源码解析之renderRoot概览中,提到了renderRoot()会调用workLoop()/workLoopSync()进行循环单元的更新:

function renderRoot(){
  xxx
  xxx
  if (workInProgress !== null) {
      //执行每个节点的更新
      if (isSync) {
        workLoopSync();
      } else {
        //判断是否需要继续调用performUnitOfWork
        workLoop();
      }
   }
  xxx
  xxx
}
复制代码

本文就来讲解workLoop及其内部的方法逻辑

注意:
本文涉及到 fiber 对象的部分属性,请参考:React源码解析之RootFiber

一、workLoop
作用:
循环执行performUnitOfWork并赋值给workInProgress,直到workInProgress值为空,则中止循环

源码:

//同步的 workLoop,说明是不可以被中断的
function workLoopSync() {
  // Already timed out, so perform work without checking if we need to yield.
  while (workInProgress !== null) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}

//异步的 workLoop,说明是可以被中断的
//判断是否需要继续调用performUnitOfWork
function workLoop() {
  // Perform work until Scheduler asks us to yield
  /*nextUnitOfWork =》workInProgress*/
  //未到达根节点时

  //有workInProgress.child的时候,一直循环,直到所有节点更新完毕
  while (workInProgress !== null && !shouldYield()) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}
复制代码

解析:
workInProgress 是一个 fiber 对象,它值不为 null 意味着该 fiber 对象上仍然有更新要执行

二、performUnitOfWork
作用:
调用beginWork,从父至子,进行组件(节点)更新;
调用completeUnitOfWork,从子至父,根据 effectTag,对节点进行一些处理

源码:

//从上至下遍历、操作节点,至底层后,再从下至上,根据effectTag 操作节点
//unitOfWork 即 workInProgress,是一个 fiber 对象
function performUnitOfWork(unitOfWork: Fiber): Fiber | null {
  // 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.
  //current <=> workInProgress
  //获取当前节点
  const current = unitOfWork.alternate;
  //在unitOfWork上做个标记,不看
  startWorkTimer(unitOfWork);
  //dev 环境,不看
  setCurrentDebugFiberInDEV(unitOfWork);

  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    //进行节点操作,并创建子节点
    //current: workInProgress.alternate
    //unitOfWork: workInProgress

    //workInProgress.child
    //判断fiber有无更新,有更新则进行相应的组件更新,无更新则复制节点
    next = beginWork(current, unitOfWork, renderExpirationTime);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, renderExpirationTime);
  }
  //不看
  resetCurrentDebugFiberInDEV();
  //将待更新的 props 替换成正在用的 props
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  //说明已经更新到了最底层的叶子节点,并且叶子节点的兄弟节点也已经遍历完
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    //当从上到下遍历完成后,completeUnitOfWork 会从下到上根据effact tag进行一些处理
    next = completeUnitOfWork(unitOfWork);
  }

  ReactCurrentOwner.current = null;
  return next;
}
复制代码

解析:
(1) 获取当前节点current
(2) 执行beginWork(current, unitOfWork, renderExpirationTime)进行组件的更新,返回的值赋给next
(3) 将unitOfWork上待更新的 props 替换成正在用的 props
(4) 如果next为 null,则执行completeUnitOfWork,从下到上遍历,并根据 effectTag 进行一些处理
(5) 最终返回next(next 为 fiber 对象)

看下beginWork()completeUnitOfWork()会在后面文章中解析。

三、beginWork
注意:
switch…case… 那超长的两段直接跳过

作用:
判断fiber有无更新,有更新则进行相应的组件更新,无更新则复制节点

源码:

//判断fiber有无更新,有更新则进行相应的组件更新,无更新则复制节点
//current: workInProgress.alternate
function beginWork(
  current: Fiber | null,
  //workInProgress创建的子节点也是workInProgress
  workInProgress: Fiber,
  //标记 该次渲染中,优先级最高的点
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  //只有当调用 react.domRender的时候,rootFiber的expirationTime才有值,rootFiber 才会更新

  //获取 fiber 对象上更新的过期时间
  const updateExpirationTime = workInProgress.expirationTime;


  //判断是不是第一次渲染
  //如果不是第一次渲染
  if (current !== null) {
    //上一次渲染完成后的props,即 oldProps
    const oldProps = current.memoizedProps;
    //新的变动带来的props,即newProps
    const newProps = workInProgress.pendingProps;

    if (
      //前后 props 是否不相等
      oldProps !== newProps ||
      //是否有老版本的 context 使用,并且发生了变化
      hasLegacyContextChanged() ||
      // Force a re-render if the implementation changed due to hot reload:
      //开发环境永远是 false
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // If props or context changed, mark the fiber as having performed work.
      // This may be unset if the props are determined to be equal later (memo).
      //判断接收到了更新 update
      didReceiveUpdate = true;
    }
    //有更新,但是优先级不高,在本次渲染过程中不需要执行,设为 false
    else if (updateExpirationTime < renderExpirationTime) {
      didReceiveUpdate = false;
      // This fiber does not have any pending work. Bailout without entering
      // the begin phase. There's still some bookkeeping we that needs to be done
      // in this optimized path, mostly pushing stuff onto the stack.
      //根据workInProgress的tag,进行相应组件的更新
      switch (workInProgress.tag) {
        case HostRoot:
          pushHostRootContext(workInProgress);
          resetHydrationState();
          break;
        case HostComponent:
          pushHostContext(workInProgress);
          if (
            workInProgress.mode & ConcurrentMode &&
            renderExpirationTime !== Never &&
            shouldDeprioritizeSubtree(workInProgress.type, newProps)
          ) {
            if (enableSchedulerTracing) {
              markSpawnedWork(Never);
            }
            // Schedule this fiber to re-render at offscreen priority. Then bailout.
            workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
            return null;
          }
          break;
        case ClassComponent: {
          const Component = workInProgress.type;
          if (isLegacyContextProvider(Component)) {
            pushLegacyContextProvider(workInProgress);
          }
          break;
        }
        case HostPortal:
          pushHostContainer(
            workInProgress,
            workInProgress.stateNode.containerInfo,
          );
          break;
        case ContextProvider: {
          const newValue = workInProgress.memoizedProps.value;
          pushProvider(workInProgress, newValue);
          break;
        }
        case Profiler:
          if (enableProfilerTimer) {
            workInProgress.effectTag |= Update;
          }
          break;
        case SuspenseComponent: {
          const state: SuspenseState | null = workInProgress.memoizedState;
          const didTimeout = state !== null;
          if (didTimeout) {
            // If this boundary is currently timed out, we need to decide
            // whether to retry the primary children, or to skip over it and
            // go straight to the fallback. Check the priority of the primary
            // child fragment.
            const primaryChildFragment: Fiber = (workInProgress.child: any);
            const primaryChildExpirationTime =
              primaryChildFragment.childExpirationTime;
            if (
              primaryChildExpirationTime !== NoWork &&
              primaryChildExpirationTime >= renderExpirationTime
            ) {
              // The primary children have pending work. Use the normal path
              // to attempt to render the primary children again.
              return updateSuspenseComponent(
                current,
                workInProgress,
                renderExpirationTime,
              );
            } else {
              pushSuspenseContext(
                workInProgress,
                setDefaultShallowSuspenseContext(suspenseStackCursor.current),
              );
              // The primary children do not have pending work with sufficient
              // priority. Bailout.
              const child = bailoutOnAlreadyFinishedWork(
                current,
                workInProgress,
                renderExpirationTime,
              );
              if (child !== null) {
                // The fallback children have pending work. Skip over the
                // primary children and work on the fallback.
                return child.sibling;
              } else {
                return null;
              }
            }
          } else {
            pushSuspenseContext(
              workInProgress,
              setDefaultShallowSuspenseContext(suspenseStackCursor.current),
            );
          }
          break;
        }
        case DehydratedSuspenseComponent: {
          if (enableSuspenseServerRenderer) {
            pushSuspenseContext(
              workInProgress,
              setDefaultShallowSuspenseContext(suspenseStackCursor.current),
            );
            // We know that this component will suspend again because if it has
            // been unsuspended it has committed as a regular Suspense component.
            // If it needs to be retried, it should have work scheduled on it.
            workInProgress.effectTag |= DidCapture;
          }
          break;
        }
        case SuspenseListComponent: {
          const didSuspendBefore =
            (current.effectTag & DidCapture) !== NoEffect;

          const childExpirationTime = workInProgress.childExpirationTime;
          if (childExpirationTime < renderExpirationTime) {
            // If none of the children had any work, that means that none of
            // them got retried so they'll still be blocked in the same way
            // as before. We can fast bail out.
            pushSuspenseContext(workInProgress, suspenseStackCursor.current);
            if (didSuspendBefore) {
              workInProgress.effectTag |= DidCapture;
            }
            return null;
          }

          if (didSuspendBefore) {
            // If something was in fallback state last time, and we have all the
            // same children then we're still in progressive loading state.
            // Something might get unblocked by state updates or retries in the
            // tree which will affect the tail. So we need to use the normal
            // path to compute the correct tail.
            return updateSuspenseListComponent(
              current,
              workInProgress,
              renderExpirationTime,
            );
          }

          // If nothing suspended before and we're rendering the same children,
          // then the tail doesn't matter. Anything new that suspends will work
          // in the "together" mode, so we can continue from the state we had.
          let renderState = workInProgress.memoizedState;
          if (renderState !== null) {
            // Reset to the "together" mode in case we've started a different
            // update in the past but didn't complete it.
            renderState.rendering = null;
            renderState.tail = null;
          }
          pushSuspenseContext(workInProgress, suspenseStackCursor.current);
          break;
        }
        case EventComponent:
          if (enableFlareAPI) {
            pushHostContextForEventComponent(workInProgress);
          }
          break;
      }
      //跳过该节点及所有子节点的更新
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  } else {
    didReceiveUpdate = false;
  }

  // Before entering the begin phase, clear the expiration time.
  workInProgress.expirationTime = NoWork;
  //如果节点是有更新的
  //根据节点类型进行组件的更新
  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderExpirationTime,
      );
    }
    case LazyComponent: {
      const elementType = workInProgress.elementType;
      return mountLazyComponent(
        current,
        workInProgress,
        elementType,
        updateExpirationTime,
        renderExpirationTime,
      );
    }
    //FunctionComponent的更新
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime,
      );
    }
    //ClassComponent的更新
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderExpirationTime);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderExpirationTime);
    case HostText:
      return updateHostText(current, workInProgress);
    case SuspenseComponent:
      return updateSuspenseComponent(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case HostPortal:
      return updatePortalComponent(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case ForwardRef: {
      const type = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === type
          ? unresolvedProps
          : resolveDefaultProps(type, unresolvedProps);
      return updateForwardRef(
        current,
        workInProgress,
        type,
        resolvedProps,
        renderExpirationTime,
      );
    }
    case Fragment:
      return updateFragment(current, workInProgress, renderExpirationTime);
    case Mode:
      return updateMode(current, workInProgress, renderExpirationTime);
    case Profiler:
      return updateProfiler(current, workInProgress, renderExpirationTime);
    case ContextProvider:
      return updateContextProvider(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case ContextConsumer:
      return updateContextConsumer(
        current,
        workInProgress,
        renderExpirationTime,
      );
    case MemoComponent: {
      const type = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      // Resolve outer props first, then resolve inner props.
      let resolvedProps = resolveDefaultProps(type, unresolvedProps);
      if (__DEV__) {
        if (workInProgress.type !== workInProgress.elementType) {
          const outerPropTypes = type.propTypes;
          if (outerPropTypes) {
            checkPropTypes(
              outerPropTypes,
              resolvedProps, // Resolved for outer only
              'prop',
              getComponentName(type),
              getCurrentFiberStackInDev,
            );
          }
        }
      }
      resolvedProps = resolveDefaultProps(type.type, resolvedProps);
      return updateMemoComponent(
        current,
        workInProgress,
        type,
        resolvedProps,
        updateExpirationTime,
        renderExpirationTime,
      );
    }
    case SimpleMemoComponent: {
      return updateSimpleMemoComponent(
        current,
        workInProgress,
        workInProgress.type,
        workInProgress.pendingProps,
        updateExpirationTime,
        renderExpirationTime,
      );
    }
    case IncompleteClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return mountIncompleteClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime,
      );
    }
    case DehydratedSuspenseComponent: {
      if (enableSuspenseServerRenderer) {
        return updateDehydratedSuspenseComponent(
          current,
          workInProgress,
          renderExpirationTime,
        );
      }
      break;
    }
    case SuspenseListComponent: {
      return updateSuspenseListComponent(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
    case EventComponent: {
      if (enableFlareAPI) {
        return updateEventComponent(
          current,
          workInProgress,
          renderExpirationTime,
        );
      }
      break;
    }
  }
  invariant(
    false,
    'Unknown unit of work tag. This error is likely caused by a bug in ' +
      'React. Please file an issue.',
  );
}
复制代码

解析:
(1) fiber 对象只要有更新,就会计算出一个 expirationTime
(2) memoizedPropspendingPropsfiber对象的属性,具体请看:React源码解析之RootFiber
(3) switch...case那段太长了,可跳过,后面会讲FunctionComponent情况时,组件是如何更新的
(4) 注意updateExpirationTime < renderExpirationTime的情况,它的意思是当前 fiber 的更新优先级不高,在本次渲染的过程中不会执行它的更新,所以会执行bailoutOnAlreadyFinishedWork,来跳过该节点及所有子节点的更新,不再往下执行组件的更新
(5) 最后根据workInProgress.tag的不同情况,来进行组件的更新

四、bailoutOnAlreadyFinishedWork
作用:
跳过该节点及该节点上所有子节点的更新

源码:

//跳过该节点及所有子节点的更新
function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  //不看
  cancelWorkTimer(workInProgress);

  if (current !== null) {
    // Reuse previous dependencies
    workInProgress.dependencies = current.dependencies;
  }

  if (enableProfilerTimer) {
    // Don't update "base" render times for bailouts.
    stopProfilerTimerIfRunning(workInProgress);
  }

  // Check if the children have any pending work.
  //expirationTime 表示该节点是否有更新,如果该节点有更新,可能会影响子节点的更新
  //如果expirationTime和childExpirationTime都没有,则子树是不需要更新的

  //由于子孙节点造成的更新
  const childExpirationTime = workInProgress.childExpirationTime;
  //如果子树不需要更新,则返回 null

  //childExpirationTime的一个好处就是快捷地知道子树有没有更新,从而跳过没有更新的子树
  //如果childExpirationTime为空,react 还需要遍历子树来判断是否更新
  if (childExpirationTime < renderExpirationTime) {
    // The children don't have any work either. We can skip them.
    // TODO: Once we add back resuming, we should check if the children are
    // a work-in-progress set. If so, we need to transfer their effects.

    //跳过整个子树的更新渲染,这是一个非常大的优化
    return null;
  }
  //调和子节点
  else {
    // This fiber doesn't have work, but its subtree does. Clone the child
    // fibers and continue.
    //该节点不需要更新,子节点也不需要更新,所以只要复制子节点过来即可
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}
复制代码

解析:
通常判断子节点的更新是要遍历子树来获取信息的,但 React 非常聪明地在子节点产生更新的时候,设置上 childExpirationTime,并最终在父节点上设置一个优先级最高的 childExpirationTime,这样的话,如果childExpirationTime优先级小于renderExpirationTime的话,则跳过子树的遍历及更新渲染。

这是一个非常大的优化。

五、completeUnitOfWork
completeUnitOfWorkbeginWork的结构是类似的,但我会放在后面的文章去讲

六、workLoop流程图

七、GitHub
github.com/AttackXiaoJ…


(完)

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改