更新时 Fiber 节点能否复用?

940 阅读2分钟

当产生更新时,workInProgressTreeFiber 节点有两种方式生成:

  1. re-render
  2. 复用 currentTreeFiber 节点

本文进行了以下探究:

更新时,能否复用 currentTreeFiber 节点这种情况。

beginWork

当调度更新时,会进入到 render 阶段,也就是产生 Fiber 的阶段,此时会调用到 beginWork 方法,该方法中对类组件和函数组件的处理如下:

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
){
    switch (workInProgress.tag) {
      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,
          renderLanes,
        );
      }
      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,
          renderLanes,
        );
      }
    }
  }

先看 ClassComponent 的产生 Fiber 的处理再看 FunctionComponent 吧。

ClassComponent

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
    shouldUpdate = updateClassInstance(
      current,
      workInProgress,
      Component,
      nextProps,
      renderLanes,
    );
    
    const nextUnitOfWork = finishClassComponent(
      current,
      workInProgress,
      Component,
      shouldUpdate,
      hasContext,
      renderLanes,
    );
    return nextUnitOfWork;
  }

finishClassComponent 方法执行后会返回 Fiber 节点,而是否可复用 currentFiber,取决于 updateClassInstance 方法的执行结果。

来看下 updateClassInstance

function updateClassInstance(
  current: Fiber,
  workInProgress: Fiber,
  ctor: any,
  newProps: any,
  renderLanes: Lanes,
) {
      const shouldUpdate =
    checkHasForceUpdateAfterProcessing() ||
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    );
    return shouldUpdate;
  }

根据 updateClassInstance 方法里,判断是否需要更新的的 2 个条件:

  1. 是否有 forceUpdate
  2. shouldComponentUpdatecheckShouldComponentUpdate

可知,ClassComponentFiber 是否可复用,取决于 2 个条件:本次更新是否是调用 forceUpdate 产生的更新和 shouldComponentUpdate 方法中根据 props 判断当次是否需要更新

function checkShouldComponentUpdate(
  workInProgress,
  ctor,
  oldProps,
  newProps,
  oldState,
  newState,
  nextContext,
) {
  const instance = workInProgress.stateNode;
  if (typeof instance.shouldComponentUpdate === 'function') {
    const shouldUpdate = instance.shouldComponentUpdate(
      newProps,
      newState,
      nextContext,
    );

    return shouldUpdate;
  }

  if (ctor.prototype && ctor.prototype.isPureReactComponent) {
    return (
      !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
    );
  }

  return true;
}

根据 checkShouldComponentUpdate 的返回可知,当没有设置 shouldComponentUpdate 方法时,其效果等同于设置了 shouldComponentUpdate 但恒返回 true ,也就是 ClassComponent 会受各种因素而无故 re-render

下面来看 Functioncomponent 的产生 Fiber 节点的处理吧。

FunctionComponent

function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
    if (current !== null && !didReceiveUpdate) {
      bailoutHooks(current, workInProgress, renderLanes);
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
}

FunctionComponent 是否可复用 Fiber ,取决于 didReceiveUpdate 这个变量,这个变量在 beginWork 方法中被赋值:

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
){
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      // 当优先度不足够时进入此判断
      didReceiveUpdate = false;
      switch(workInProgress.tag) {
            // ...
        } 
        // 有可能会执行到这,如果上面的 switch 没有 return 语句的话
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    } else {
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
}

由以上可知,当 FunctionComponentpropscontext(当处于 dev 时,需要判断元素类型 type )未变化,且本次更新的优先级足够时,didReceiveUpdate 变量会设置为 false,在接下来的 updateFunctionComponent 方法的执行后会返回可复用的 Fiber 节点。

总结

更新时,workInProgressTree 能否复用 currentTreeFiber 节点取决于:

  1. ClassComponent
    1. 本次更新不是调用 forceUpdate 来更新的
    2. shouldComponentUpdate 中对更新的属性进行判断来决定本次更新是不需更新的
  2. FunctionComponent
    1. dev 时,元素类型 type 不变
    2. props 不变
    3. context 没有更新
    4. 本次优先级足够

参考

[ beginWork / updateFunctionComponent / updateClassComponent / finishClassComponent] github.com/facebook/re…

[updateClassInstance / checkShouldComponentUpdate ] github.com/facebook/re…

[checkHasForceUpdateAfterProcessing] github.com/facebook/re…