zl程序教程

您现在的位置是:首页 >  Java

当前栏目

从源码层次了解 React 生命周期:更新

2023-02-18 16:37:41 时间

大家好,我是前端西瓜哥。

今天我们继续从源码层面看 React 的更新阶段,是如何触发类函数的生命周期函数的。

上一篇:

从源码层次了解 React 生命周期:挂载

React 版本为 18.2.0。 为了更关注本文中的代码片段省略了大量的细致末节,并直接丢掉函数的参数。

上一篇文章说了挂载过程中,React 底层是如何调用类组件的生命周期函数的。这次就说说更新的情况。

还是这个图:

image-20221124135036981

这次我们讲解更新阶段。触发顺序为:

  1. componentWillReceiveProps(废弃,所以上图中没有,但还是要说说)
  2. static getDerivedStateFromProps
  3. shouldComponentUpdate
  4. componentWillUpdate(废弃,所以上图中没有,但还是要说说)
  5. render
  6. getSnapshotBeforeUpdate
  7. componentDidUpdate

componentWillReceiveProps

updateClassComponent 源码位置: https://github.com/facebook/react/blob/1cd90d2ccc791f3ed25d93ceb7137746185f6e34/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L1093 callComponentWillReceiveProps 源码位置: https://github.com/facebook/react/blob/1cd90d2ccc791f3ed25d93ceb7137746185f6e34/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L1142

componentWillReceiveProps 会在父组件更新时被调用,并拿到最新的 props 和 context,此时实例的属性还未变更。

首先调用 updateClassComponent 方法,这个方法会根据条件走向 “挂载”(mountClassInstance)或 “更新”(updateClassInstance)。下面代码的 instance 就是类组件实例,如果已经创建了实例,说明。

function updateClassComponent() {
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  if (instance === null) { // 初始化组件
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else if (current === null) { // 类组件实例存在,但 fiber 没构建的情况,会进行复用
    shouldUpdate = resumeMountClassInstance(workInProgress, Component, nextProps, renderLanes);
  } else { // 更新组件
    shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderLanes);
  }
}

更新阶段,会执行 updateClassInstance 方法。

updateClassInstance 里其实会执行绝大多数的生命周期钩子,只要是在操作真实 DOM 前的都会调用。

updateClassInstance 首先会 调用 callComponentWillReceiveProps 方法:

function updateClassInstance() {
  // 1. 调用 componentWillReceiveProps
  if (oldProps !== newProps || oldContext !== nextContext) {
    callComponentWillReceiveProps(
      workInProgress,
      instance,
      newProps,
      nextContext,
    );
  }
}

可以看到,必须 props 变成新的,或者 context 是新的,才会调用 componentWillReceiveProps。

callComponentWillReceiveProps 的核心代码为:

function callComponentWillReceiveProps() {
  instance.componentWillReceiveProps(newProps, nextContext);
  instance.UNSAFE_componentWillReceiveProps(newProps, nextContext);
}

调用栈(在 componentWillReceiveProps 上加上 console.trace('componentWillReceiveProps')):

image-20221125213838158

static getDerivedStateFromProps

applyDerivedStateFromProps 源码地址: https://github.com/facebook/react/blob/1cd90d2ccc791f3ed25d93ceb7137746185f6e34/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L163

componentWillReceiveProps 是类静态方法,会在组件更新时,拿到最新的 props 以及最新的 state,该函数的返回值会合并到 state 对象上。

需要注意的是,因为它是类静态方法,所以无法拿到 this.state 或者 this.props。

还是 updateComponentInstance,紧随着 componentWillReceiveProps 调用 applyDerivedStateFromProps,然后调用 componentWillReceiveProps 方法将返回的对象组合到类组件实例的 state 中。

function updateClassInstance() {
  // 1. 调用 componentWillReceiveProps

  // 2. 调用 getDerivedStateFromProps
  const getDerivedStateFromProps = ctor.getDerivedStateFromProps;
  applyDerivedStateFromProps(
    workInProgress,
    ctor,
    getDerivedStateFromProps,
    newProps,
  );
}

applyDerivedStateFromProps 和上一篇的挂载阶段流程一致。

function applyDerivedStateFromProps(
  workInProgress,
  ctor,
  getDerivedStateFromProps,
  nextProps,
) {
  const prevState = workInProgress.memoizedState;
  let partialState = getDerivedStateFromProps(nextProps, prevState);
  // 合并
  const memoizedState =
    partialState === null || partialState === undefined
      ? prevState
      : assign({}, prevState, partialState);

  workInProgress.memoizedState = memoizedState;
}

调用栈:

image-20221125101044583

shouldComponentUpdate

checkShouldComponentUpdate 源码地址: https://github.com/facebook/react/blob/1cd90d2ccc791f3ed25d93ceb7137746185f6e34/packages/react-reconciler/src/ReactFiberClassComponent.new.js#L308

shouldComponet 会接收新 props 和 state,我们将它们与当前的 props 和 state 对比,返回一个布尔值,决定是否应用新的 props 和 state,并重渲染组件。

updateClassInstance 方法下,会调用一些方法判断是否允许更新组件,有:

  1. checkHasForceUpdateAfterProcessing():this.forceUpdate() 会将一个名为 hasForceUpdate 的变量临时变成 true,checkHasForceUpdateAfterProcessing 方法会返回该变量;
  2. checkShouldComponentUpdate():这个方法会调用类组件实例的 shouldComponentUpdate。
function updateClassInstance() {
  // 1. 调用 componentWillReceiveProps

  // 2. 调用 getDerivedStateFromProps
    
  // 是否应该更新组件
  // 3. 调用 shouldComponentUpdate
 const shouldUpdate =
    // 是否为强制更新(对应 this.forceUpdate())
    checkHasForceUpdateAfterProcessing() ||
    // 调用 shouldComponentUpdate
    checkShouldComponentUpdate(
      workInProgress,
      ctor,
      oldProps,
      newProps,
      oldState,
      newState,
      nextContext,
    ) ||
    // context  相关
    (enableLazyContextPropagation &&
      current !== null &&
      current.dependencies !== null &&
      checkIfContextChanged(current.dependencies));

}

checkShouldComponentUpdate 的核心代码:

function checkShouldComponentUpdate() {
  const instance = workInProgress.stateNode;
  let shouldUpdate = instance.shouldComponentUpdate(
    newProps,
    newState,
    nextContext,
  );
  return shouldUpdate;
}

调用栈:

image-20221125154311755

componentWillUpdate

紧接着前面代码中拿到的 shouldUpdate 值,如果是真值,就会执行 componentWillUpdate。

function updateClassInstance() {
  // 1. 调用 componentWillReceiveProps

  // 2. 调用 getDerivedStateFromProps
    
  // 是否应该更新组件
  // 3. 调用 shouldComponentUpdate
 shouldUpdate = true; // 假设为 true
 
  // 如果 shouldUpdate 为真值,
  // 4. 调用 componentWillUpdate
  if (shouldUpdate) {
    instance.componentWillUpdate(newProps, newState, nextContext);
    instance.UNSAFE_componentWillUpdate(newProps, newState, nextContext);
  }
  
  instance.props = newProps;
  instance.state = newState;
  instance.context = nextContext;
  
  return shouldUpdate;
}

题外话,React 还会在这里打标签,标记之后是否要执行 componentDidUpdate 和 getSnapshotBeforeUpdate 方法。

workInProgress.flags |= Update;
workInProgress.flags |= Snapshot;

调用栈:

image-20221125161822580

render

走完前面的 updateClassInstance 函数,我们拿到一个代表是否要更新组件的 shouldUpdate 布尔值。

接着我们就 调用 finishClassComponent 方法。

function updateClassComponent() {
  const instance = workInProgress.stateNode;
  let shouldUpdate;
  if (instance === null) {
  // ...
  } else if (current === null) {
    // ...
  } else { // 更新组件
    shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderLanes);
  }
  
  // 调用 finishClassComponent 方法,传入了 shouldUpdate
  const nextUnitOfWork = finishClassComponent(
    current,
    workInProgress,
    Component,
    shouldUpdate,
    hasContext,
    renderLanes,
  );
}

finishClassComponent 核心代码:

function finishClassComponent() {
  // 如果 shouldUpdate 为假,停止更新
  if (!shouldUpdate) {
    // Context providers should defer to sCU for rendering
    if (hasContext) {
      invalidateContextProvider(workInProgress, Component, false);
    }
    
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  // shouldUpdate 为真,走正常流程
  // 5. 调用 render
  nextChildren = instance.render();
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}

调用栈:

image-20221125162338612

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 的执行时间是在接近更新 DOM 前,该方法会拿到更新前的 state 和 props,并会返回值缓存起来,在 componetDidUpdate 中获取。

从这个方法开始,就进入了 commit 阶段,具体是在 commitBeforeMutationEffectsOnFiber 方法下。

逻辑不复杂,首先拿到类组件实例 instance,调用它的 getSnapshotBeforeUpdate 方法,传入 prevProps 和 prevState。

然后将返回的值保存到 instance.__reactInternalSnapshotBeforeUpdate 下,准备给 componetDidUpdate 用。

function commitBeforeMutationEffectsOnFiber(finishedWork) {
  const prevProps = current.memoizedProps;
  const prevState = current.memoizedState;
  const instance = finishedWork.stateNode;
  
  switch (finishedWork.tag) {
    case ClassComponent: {
      const snapshot = instance.getSnapshotBeforeUpdate(
        finishedWork.elementType === finishedWork.type
          ? prevProps
          : resolveDefaultProps(finishedWork.type, prevProps),
        prevState,
      );
   // 保存到类组件实例的 __reactInternalSnapshotBeforeUpdate 上
      instance.__reactInternalSnapshotBeforeUpdate = snapshot;

      break;
    }
  }
}

调用栈:

image-20221125172551556

componentDidUpdate

componentDidUpdate 会在 DOM 更新后被调用,接受三个参数:

  1. prevProps:上一个 props;
  2. prevState:上一个 state;
  3. snapshot:前面提到的那个 snapshot;

具体是在 commitLayoutEffectOnFiber 方法内,调用 componentDidUpdate 方法。

function commitLayoutEffectOnFiber(finishedRoot, current, finishedWork, committedLanes) {
  switch (finishedWork.tag) {
    case ClassComponent: {
      if (current === null) {
        // 挂载
        instance.componentDidMount();
      } else {
        // 更新
        instance.componentDidUpdate(
          prevProps,
          prevState,
          instance.__reactInternalSnapshotBeforeUpdate,
        );
      }
      break;
    }
  }
}

调用栈:

image-20221125173934467

图示

按照惯例,画个简单的流程图。

image-20221125212312197

结尾

我是前端西瓜哥,欢迎关注我,学习更多前端知识。