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

45 阅读3分钟

文章首发于我的公众号:前端西瓜哥

大家好,我是前端西瓜哥。今天我们看看组件挂载时,React 底层是如何调用我们类组件的生命周期函数的。

React 源码使用的是 18.2.0 版本。

React 在重构的过程中,改了很多内部的方法名,如果你在旧版本的 React 源码里查找文章提及的内部方法,可能会找不到。

下面的代码都是去掉了细枝末节的,只保留和生命周期相关的逻辑。

今天来看看一个类组件在挂载的时候,react 源码层面发生了什么事情。

我们先看流程图:

image-20221124135036981

本文只讲解挂载阶段。

挂载阶段源码分析

挂载(mount)阶段,依次执行的方法顺序为:

  1. constructor;
  2. static getDerivedStateFromProps;
  3. componentWillMount(已废弃,不建议使用。图上没写,但 componentWillMount 是在 render 前执行的);
  4. render;
  5. componentDidMount;

constructor

首先是对类组件进行实例化。

react-reconiler 包中的 constructClassInstance 方法:

function constructClassInstance(
  workInProgress,
  ctor,
  props
) {
  let instance = new ctor(props, context);
  // ...
}

这个 ctor(constructor 的缩写)就是我们的类组件,也就是一个 class,我们会 new 一下它生成实例,并挂载到 fiber.stateNode 上。

我们在 componentDidMount 用 console.trace('constructor') 打印调用栈,可以得到下面结果。

image-20221124104158122

getDerivedStateFromProps

getDerivedStateFromProps 是类静态方法,可以拿到最新的 props,然后将返回的对象合并到 state 上。

调用完 constructClassInstance 方法中后,紧接着就会调用一个叫做 mountClassInstance 的方法。

function mountClassInstance(
  workInProgress,
  ctor,
  newProps,
  renderLanes,
) {
  // 调用类静态方法 getDerivedStateFromProps
	applyDerivedStateFromProps(
    workInProgress,
    ctor,
    getDerivedStateFromProps,
    newProps,
  );
  // 这里还有执行 componentWillMount 逻辑,后面再说
}

mountClassInstance 下会 调用 applyDerivedStateFromProps,这个方法会调用类的 getDerivedStateFromProps 静态方法,得到 partialState 合并到 state 上。

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-20221124104355043

componentWillMount

componentWillMount 已废弃,不建议使用,且改名为 UNSAFE_componentWillMount。

该函数只会在 getDerivedStateFromProps 和 getSnapshotBeforeUpdate 不存在时才会触发。

这个逻辑还是在 mountClassInstance 下。

function mountClassInstance(
  workInProgress,
  ctor,
  newProps,
  renderLanes,
) {
  // 调用类静态方法 getDerivedStateFromProps
  // ...
  // 这里还有执行 componentWillMount
  if (
    typeof ctor.getDerivedStateFromProps !== 'function' &&
    typeof instance.getSnapshotBeforeUpdate !== 'function' &&
    (typeof instance.UNSAFE_componentWillMount === 'function' ||
      typeof instance.componentWillMount === 'function')
  ) {
    callComponentWillMount(workInProgress, instance);
  }
}

callComponentWillMount 核心代码为:

function callComponentWillMount(workInProgress, instance) {
  if (typeof instance.componentWillMount === 'function') {
    instance.componentWillMount();
  }
  if (typeof instance.UNSAFE_componentWillMount === 'function') {
    instance.UNSAFE_componentWillMount();
  }
}

有意思,React 底层还是同时兼容旧的 componentWillMount 写法的,新旧两种 API 都会被调用。但会在控制台打印警告信息就是了。

打印一下调用栈:

image-20221124104755174

render

接着就是 调用 finishClassComponent 方法,调用类组件实例的 render 方法,拿到 reactElement。接着调用 reconcileChildren 递归处理调和子元素。

function finishClassComponent(
  current,
  workInProgress,
  Component,
  shouldUpdate,
  hasContext,
  renderLanes,
) {
  const instance = workInProgress.stateNode;
  // 调用 render 拿到 
  nextChildren = instance.render();
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
}

打印一下调用栈:

image-20221124105113332

componentDidMount

componentDidMount 会在 DOM 更新后调用。

前面的方法都是在 render 阶段调用,它们是同步发生的。

而 componentDidMount 则是在 commit 阶段执行,是异步的。React 会在所有的 fiber 调和(reconcile)完后进入 commit 阶段。

具体在 commitLayoutEffectOnFiber 内调用

function commitLayoutEffectOnFiber(
  finishedRoot,
  current,
  finishedWork,
  committedLanes,
) {
  switch (finishedWork.tag) {
    // ...
    case ClassComponent: {
      const instance = finishedWork.stateNode;
      if (current === null) {
        // 调用实例的 componentDidMount
      	instance.componentDidMount();
      }
      break;
    }
    // ...
  }
}

打印下调用栈:

image-20221124102944059

图示说明

画个图,更好地理解这个流程。

image-20221124120540330

结尾

至此,React 中的一个类组件的挂载过程调用完所有的生命周期函数。

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