React 源码解读之class组件更新updateClassComponent (五)

291 阅读9分钟

这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。

react 版本:v17.0.3

ClassComponent的三种更新情形

在《React 源码解读之class组件更新updateClassComponent (一)》一文中我们提到,对于 ClassComponent 的更新,有三种情形:

  • 情形一:ClassComponent实例未被创建,此时会调用 constructClassInstance 方法构建class组件实例,然后调用 mountClassInstance 方法挂载class组件,并将 shouldUpdate 置为 true,标记组件需要更新渲染。

  • 情形二:ClassComponent实例已经存在,但current(当前渲染在界面上的fiber树) 为null,即 ClassComponent 是初次渲染,此时调用 resumeMountClassInstance 方法,复用ClassComponent实例,并更新 state/props,然后会执行 componentWillMount 生命周期函数。

  • 情形三:ClassComponent实例已经存在,且已经是多次渲染,此时调用 updateClassInstance 方法执行更新操作,且会执行 componentWillUpdate 生命周期函数。

执行完这三种更新后,updateClassComponent 最后执行了 finishClassComponent() 函数来判断是否需要 执行render 生命周期函数,即是否需要渲染组件。

// react-reconciler/src/ReactFiberBeginWork.new.js

// 判断是否执行 render 生命周期函数
const nextUnitOfWork = finishClassComponent(
  current,
  workInProgress,
  Component,
  shouldUpdate,
  hasContext,
  renderLanes,
);

执行 render,渲染节点 -- finishClassComponent

finishClassComponent 做的事情就是判断 是否需要执行 render,渲染当前节点 ,并返回当前渲染节点的下一个节点。

// react-reconciler/src/ReactFiberBeginWork.new.js

function finishClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  shouldUpdate: boolean,
  hasContext: boolean,
  renderLanes: Lanes,
) {
  // Refs should update even if shouldComponentUpdate returns false
  // 无论 props/state 是否有更新,都需要更新 ref 指向
  markRef(current, workInProgress);

  // 错误捕获
  const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;

  if (!shouldUpdate && !didCaptureError) {
    // Context providers should defer to sCU for rendering
    if (hasContext) {
      invalidateContextProvider(workInProgress, Component, false);
    }

    // 当不需要更新或者已经更新完成,并且没有出现 error 的时候
    // 跳过该 class 上的节点及所有子节点的更新,也就是跳过 render 方法
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }

  // 获取class组件实例   
  const instance = workInProgress.stateNode;

  // Rerender
  ReactCurrentOwner.current = workInProgress;
  let nextChildren;

  //getDerivedStateFromError 是生命周期api,作用是捕获 render error
  // 详情请看:https://zh-hans.reactjs.org/docs/react-component.html#static-getderivedstatefromerror
  
  // 在 render 过程中如果出现了 error 但开发者没有调用 getDerivedStateFromError,则中断渲染
  if (
    didCaptureError &&
    typeof Component.getDerivedStateFromError !== 'function'
  ) {
    // If we captured an error, but getDerivedStateFromError is not defined,
    // unmount all the children. componentDidCatch will schedule an update to
    // re-render a fallback. This is temporary until we migrate everyone to
    // the new API.
    // TODO: Warn in a future release.

    // 中断渲染
    nextChildren = null;

    if (enableProfilerTimer) {
      stopProfilerTimerIfRunning(workInProgress);
    }
  } else {
    if (enableSchedulingProfiler) {
      markComponentRenderStarted(workInProgress);
    }
    
    // 执行渲染,即执行 render 生命周期函数
    if (__DEV__) {
      setIsRendering(true);
      nextChildren = instance.render();
      if (
        debugRenderPhaseSideEffectsForStrictMode &&
        workInProgress.mode & StrictLegacyMode
      ) {
        setIsStrictModeForDevtools(true);
        try {
          instance.render();
        } finally {
          setIsStrictModeForDevtools(false);
        }
      }
      setIsRendering(false);
    } else {
      nextChildren = instance.render();
    }
    if (enableSchedulingProfiler) {
      markComponentRenderStopped();
    }
  }

  // React DevTools reads this flag.
  workInProgress.flags |= PerformedWork;
  if (current !== null && didCaptureError) {
    // If we're recovering from an error, reconcile without reusing any of
    // the existing children. Conceptually, the normal children and the children
    // that are shown on error are two different sets, so we shouldn't reuse
    // normal children even if their identities match.

    // 重新计算 children,因为当出错时,是渲染到节点上的 state/props 出现了问题,所以不能复用,必须重新 render
    forceUnmountCurrentAndReconcile(
      current,
      workInProgress,
      nextChildren,
      renderLanes,
    );
  } else {
    // 进入协调过程
    // 将 ReactElement 变成 fiber 对象,并更新,生成对应的 DOM 实例,并挂载到真正的 DOM 节点上
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }

  // Memoize state using the values we just used to render.
  // TODO: Restructure so we never read values from the instance.
  workInProgress.memoizedState = instance.state;

  // The context might have changed so we need to recalculate it.
  if (hasContext) {
    invalidateContextProvider(workInProgress, Component, true);
  }

  // 返回下一个要渲染的节点
  return workInProgress.child;
}

在 finishClassComponent 函数中,做了以下事情:

1、首先是更新 ref 指向,无论 props/state 是否有更新,都要更新 ref 的指向。代码如下:

// 无论 props/state 是否有更新,都需要更新 ref 指向
markRef(current, workInProgress);

2、根据fiber节点的 flags 判断是否有错误捕获,将其赋值给 didCaptureError 变量。代码如下:

// 错误捕获
const didCaptureError = (workInProgress.flags & DidCapture) !== NoFlags;

3、如果不需要更新或者已经更新完成,并且没有错误捕获时,执行 bailoutOnAlreadyFinishedWork 方法跳过该 class组件上的节点及其所有子节点的更新,也就是跳过class组件的 render 方法。代码如下:

if (!shouldUpdate && !didCaptureError) {
  // Context providers should defer to sCU for rendering
  if (hasContext) {
    invalidateContextProvider(workInProgress, Component, false);
  }

  // 当不需要更新或者已经更新完成,并且没有出现 error 的时候
  // 跳过该 class 上的节点及所有子节点的更新,也就是跳过 render 方法
  return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}

4、在执行 render 的过程中,如果捕获到了 error,并且开发者也没有调用 getDerivedStateFromError 生命周期函数,则中断渲染,将 nextChildren 设为 null 。代码如下:

  // 在 render 过程中如果出现了 error 但开发者没有调用 getDerivedStateFromError,则中断渲染
  if (
    didCaptureError &&
    typeof Component.getDerivedStateFromError !== 'function'
  ) {
    // If we captured an error, but getDerivedStateFromError is not defined,
    // unmount all the children. componentDidCatch will schedule an update to
    // re-render a fallback. This is temporary until we migrate everyone to
    // the new API.
    // TODO: Warn in a future release.

    // 中断渲染
    nextChildren = null;

    if (enableProfilerTimer) {
      stopProfilerTimerIfRunning(workInProgress);
    }
  }

5、如果没有捕获到 error,则执行class组件的 render 方法,渲染当前节点,并返回 nextChildren。代码如下:

else {
  if (enableSchedulingProfiler) {
    markComponentRenderStarted(workInProgress);
  }

  // 执行渲染,即执行 render 生命周期函数
  if (__DEV__) {
    
    // 删除了DEV 部分的代码
    
  } else {
    nextChildren = instance.render();
  }
  
  // ...
}

6、如果在渲染过程中捕获到了error,则执行 forceUnmountCurrentAndReconcile 函数,重新计算children。代码如下:

if (current !== null && didCaptureError) {

  // 重新计算 children,因为当出错时,是渲染到节点上的 state/props 出现了问题,所以不能复用,必须重新 render
  forceUnmountCurrentAndReconcile(
    current,
    workInProgress,
    nextChildren,
    renderLanes,
  );
}

7、如果在渲染过程中没有error,则执行 reconcileChildren 函数进入协调过程,将 ReactElement 变成 fiber 对象并更新,生成对应的 DOM 实例,并挂载到真正的 DOM 节点上。代码如下:

 else {
    // 进入协调过程
    // 将 ReactElement 变成 fiber 对象,并更新,生成对应的 DOM 实例,并挂载到真正的 DOM 节点上
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }

8、最后更新workInProgress的memoizedState,然后返回当前渲染节点的下一个节点。代码如下:

// Memoize state using the values we just used to render.
// TODO: Restructure so we never read values from the instance.
workInProgress.memoizedState = instance.state;

// The context might have changed so we need to recalculate it.
if (hasContext) {
  invalidateContextProvider(workInProgress, Component, true);
}

// 返回下一个要渲染的节点
return workInProgress.child;

接下来,我们来看看在 finishClassComponent 中调用的几个重要函数。

bailoutOnAlreadyFinishedWork

bailoutOnAlreadyFinishedWork 函数做的事情,就是跳过该 class组件上的节点及所有子节点的更新,也就是跳过 render 方法。

// react-reconciler/src/ReactFiberBeginWork.new.js

function bailoutOnAlreadyFinishedWork(
    current: Fiber | null,
    workInProgress: Fiber,
    renderLanes: Lanes,
  ): Fiber | null {
    if (current !== null) {
      // Reuse previous dependencies
      // 获取 current 树上的依赖,添加到当前工作的fiber上,从而复用之前的依赖
      workInProgress.dependencies = current.dependencies;
    }

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

    markSkippedUpdateLanes(workInProgress.lanes);

    // Check if the children have any pending work.
    // 检查子树是否需要更新,如果子树不需要更新,返回 null
    if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
      // 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.

      if (enableLazyContextPropagation && current !== null) {
        // Before bailing out, check if there are any context changes in
        // the children.
        // 在退出之前,检查子树是否有 context 发生变化   
        lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
        // 子树 context 没有发行变化   
        if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
          return null;
        }
      } else {
        return null;
      }
    }

    // This fiber doesn't have work, but its subtree does. Clone the child
    // fibers and continue.
    // 子树需要更新,拷贝一份 workInProgress.child 返回到 workLoop 的 nextUnitOfWork
    cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }

可以看到,在 bailoutOnAlreadyFinishedWork 函数中,检查了子树是否需要更新,如果不需要更新,返回 null,跳过当前节点及子节点的更新,即跳过组件的render方法,不执行渲染。如果子树需要更新,则拷贝一份 workInProgress.child 返回到 workLoop 的 nextUnitOfWork。

forceUnmountCurrentAndReconcile

forceUnmountCurrentAndReconcile 函数做的事情,就是在渲染过程中出错时,重新计算 children,然后重新执行渲染。

// react-reconciler/src/ReactFiberBeginWork.new.js

function forceUnmountCurrentAndReconcile(
  current: Fiber,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
  
) {

  // 执行两遍协调过程
 
  // reconcileChildFibers 函数是 reconcileChildren 的副本,在 reconcileChildren 中会调用 reconcileChildFibers 

  // This function is fork of reconcileChildren. It's used in cases where we
  // want to reconcile without matching against the existing set. This has the
  // effect of all current children being unmounted; even if the type and key
  // are the same, the old child is unmounted and a new child is created.
  //
  // To do this, we're going to go through the reconcile algorithm twice. In
  // the first pass, we schedule a deletion for all the current children by
  // passing null.
    
  // 第一遍删除所有当前节点的所有子节点
  workInProgress.child = reconcileChildFibers(
    workInProgress,
    current.child,
    null,
    renderLanes,
  );
    
   
  // In the second pass, we mount the new children. The trick here is that we
  // pass null in place of where we usually pass the current child set. This has
  // the effect of remounting all children regardless of whether their
  // identities match.
    
  // 第二遍重新挂载所有的子节点 
  workInProgress.child = reconcileChildFibers(
    workInProgress,
    null,
    nextChildren,
    renderLanes,
  );
}

在 forceUnmountCurrentAndReconcile 函数中,安排了两遍协调过程,第一遍是删除当前节点的所有子节点,第二遍是重新挂载所有的子节点。

reconcileChildFibers 函数可以理解为是 reconcileChildren 的副本,reconcileChildFibers 会在 reconcileChildren 函数中被调用,详情请阅读《React源码解读之Diff算法》中的「结合源码解读 diff」小节。

reconcileChildren

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {

  // mountChildFibers 和 reconcileChildFibers 都是调用 ChildReconciler函数
  // mountChildFibers 调用 ChildReconciler函数时传入了false,表示不需要进行 diff,直接生成新的fiber
  // reconcileChildFibers 调用 ChildReconciler函数时传入了true,表示需要进行 diff

  if (current === null) {
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.

    // 如果当前 fiber 节点 为空,则直接将新的 ReactElement 内容生成新的 fiber
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.

    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.

    // 当前fiber节点不为空,则与新生成的 ReactElement 内容进行diff
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

在渲染过程中没有error,会执行 reconcileChildren 函数进入协调过程。从源码中可以看到,在协调时,根据 current 是否存在,分别调用了mountChildFibers 函数和 reconcileChildFibers 函数。其实这两个方法调用的都是 ChildReconciler() 方法,只是传参不同。详情请阅读《React源码解读之Diff算法》中的「结合源码解读 diff」小节。

流程图

总结

本文是 React 源码解读之class组件更新updateClassComponent 系列的最后一篇。

updateClassComponent中,无论执行完哪种更新,最后都会执行 finishClassComponent 函数来判断是否需要 执行render 生命周期函数,即是否需要渲染组件。如果节点不需要更新,则执行 bailoutOnAlreadyFinishedWork 函数跳过更新,即跳过render方法。否则执行 render 方法执行更新。在执行更新的过程中,如果没有发生错误,则执行 reconcileChildren 函数进入协调流程,否则就中断渲染。