React 源码:类组件增强——生命周期函数

最近在学习慕课网 手写 React 高质量源码迈向高阶开发,之前自己也尝试看过源码,不过最终放弃了

放弃的最主要原因是 react 内部的调用链太长了,每天在缕清调用链上都花了不少时间,createRoot 都没有看完

最近看到慕课网有一个 react 源码课,就想着跟着课程然后在自己源码,看看这次能够看到什么地步

它这个课程前八章 是 react@16 的源码,从第九章开始才是 react@18 的源码

React 源码系列:

通过上面的学习,我们已经可以实现了 react 中的渲染和更新

接下来我们来实现 react 中的生命周期函数

其实生命周期函数的本质是回调函数,在特定的时机被调用,执行特定的逻辑

componentDidMount

componentDidMount 是在组件初始化时调用,可以在这里进行一些初始化的操作:

  1. 它是在组件挂载到页面上后调用
  2. 可以操作 DOM,也就是说可以使用 document.getElementById(xxx)
  3. 可以进行网络请求
  4. 可以做事件订阅,但需要在 componentWillUnmount 中取消订阅
  5. 不适合在这里调用 setState,会触发一次更新,state 初始值最好在 constructor 中赋值

我们应该在那里调用 componentDidMount 呢?

根据上面的分析,应该是在 DOM 挂载到页面之后调用

DOM 是在什么时候挂载的呢?

通过查看之前的源码,我们发现是在 mount 函数中完成 DOM 的挂载

js 复制代码
function mount(VNode, containerDOM) {
  let newDOM = createDOM(VNode);
  newDOM && containerDOM.appendChild(newDOM);
}

componentDidMount 函数是在类组件中的,我们处理类组件时,需要将类组件的实例传递出来

js 复制代码
function getDomByClassComponent(VNode) {
  let instance = new type(props);
  // ...
  let dom = createDOM(renderVNode);
  return { dom, instance };
}
js 复制代码
function createDOM(VNode) {
  // ...
  if (
    typeof type === "function" &&
    $$typeof === REACT_ELEMENT &&
    type.IS_CLASS_COMPONENT
  ) {
    const classComponent = getDomByClassComponent(VNode);
    // 将实例保存到全局
    classComponentInstance = classComponent.instance;
    return classComponent.dom;
  }
  // ...
}

然后我们在 mount 函数中调用,类组件的 componentDidMount 函数

js 复制代码
function mount(VNode, containerDOM) {
  let newDOM = createDOM(VNode);
  // 将 dom 挂载到页面
  newDOM && containerDOM.appendChild(newDOM);
  // 如果存在生命周期函数 componentDidMount 函数,调用
  if (classComponentInstance && classComponentInstance.componentDidMount)
    classComponentInstance.componentDidMount();
}

componentDidUpdate

componentDidUpdate 是在组件更新时调用,可以在这里进行一些更新后的操作:

  1. 更新完成后调用,初始化渲染不会调用
  2. 当组件完成更新,需要对 DOM 进行某种操作的时候,适合在这个函数中进行
  3. props 有所变化时,可以进行一些操作,比如网络请求
  4. 这里虽然可以调用 setState,但这是有条件的调用,否则会陷入死循环
  5. 如果 shouldComponentUpdate 返回 falsecomponentDidUpdate 不会执行
  6. 如果实现了 getSnapshotBeforeUpdatecomponentDidUpdate 会接收第三个参数
  7. 如果将 props 中的内容拷贝到 state,可以考虑直接使用 props,而不是在这里进行拷贝,参见文档:Common Bugs When Using Derived State

我们应该在哪里调用 componentDidUpdate 呢?

通过代码分析,调用 componentDidUpdate 的时机是在 DOM 更新完成时

DOM 更新完成时又是在哪里呢?

通过查看代码,我们找到是在 updateDomTree 函数中完成的,所以我们应该在 updateDomTree 函数执行完成后调用 componentDidUpdate

js 复制代码
class Component {
  update() {
    // 拿到 oldVNode
    let oldVNode = this.oldVNode;
    // 将 oldVNode 转换成真实 DOM
    let oldDOM = findDOMByVNode(oldVNode);
    // 调用 render 方法,得到新的 VNode
    let newVNode = this.render();
    // 更新 DOM,并将新的 DOM 挂载到页面上
    updateDomTree(oldVNode, newVNode, oldDOM);
  }
}

在调用 componentDidUpdate 函数前,需要对 update 方法进行改造

通过阅读官方文档,我们知道 componentDidUpdate 函数接收三个参数:

  • prevProps
  • prevState
  • snapshot(这里暂时先不考虑这个参数,在后面实现 getSnapshotBeforeUpdate 函数时再来传递)

update 函数中,我们没法拿到 prevPropsprevState 这张两个参数

那我们看下是谁调用了 update 函数,往上查找后发现是在 Updater.launchUpdate 中调用的

js 复制代码
class Updater {
  launchUpdate() {
    const { ClassComponentInstance, pendingStates } = this;
    // state 合并,只合并第一层
    ClassComponentInstance.state = pendingStates.reduce(
      (state, partialState) => {
        return { ...state, ...partialState };
      },
      ClassComponentInstance.state
    );
    // 清空 pendingStates
    pendingStates.length = 0;
    // 更新视图
    ClassComponentInstance.update();
  }
}

Updater.launchUpdate 函数实现了 state 合并,那合并前的 state 不就是 prevState

在这里我们就拿到了 prevStateprevProps

js 复制代码
class Updater {
  launchUpdate() {
    const { ClassComponentInstance, pendingStates } = this;
    // 拿到 preState 和 preProps
    const preState = ClassComponentInstance.state;
    const preProps = ClassComponentInstance.props;
    // state 合并,只合并第一层
    ClassComponentInstance.state = pendingStates.reduce(
      (state, partialState) => {
        return { ...state, ...partialState };
      },
      ClassComponentInstance.state
    );
    // 清空 pendingStates
    pendingStates.length = 0;
    // 更新视图,将 preProps 和 preState 传递给 update 函数
    ClassComponentInstance.update(preProps, preState);
  }
}

然后我们在 update 函数中调用 componentDidUpdate 函数

js 复制代码
class Component {
  update(preProps, preState) {
    // 拿到 oldVNode
    let oldVNode = this.oldVNode;
    // 将 oldVNode 转换成真实 DOM
    let oldDOM = findDOMByVNode(oldVNode);
    // 调用 render 方法,得到新的 VNode
    let newVNode = this.render();
    // 更新 DOM,并将新的 DOM 挂载到页面上
    updateDomTree(oldVNode, newVNode, oldDOM);
    // 如果存在生命周期函数 componentDidUpdate 函数,调用,并传入 preProps 和 preState
    if (this.componentDidUpdate) this.componentDidUpdate(preProps, preState);
  }
}

componentWillUnmount

componentWillUnmount 是在组件卸载时调用,可以在这里进行一些清理操作:

  1. 组件从 DOM 树上卸载完成的时候调用该函数
  2. 执行一些清理操作,比如清除定时器,取消事件订阅,取消网络请求等
  3. 不要在这里调用 setState,不会产生任何效果,卸载后不会重新渲染

我们应该在哪里调用 componentWillUnmount 呢?

通过代码分析,调用 componentWillUnmount 的时机是在 DOM 卸载完成时

DOM 卸载完成时又是在哪里呢?

通过查看代码我们找到了 removeVNode,这个函数是在删除或者替换 DOM 时调用的

js 复制代码
function removeVNode(VNode) {
  // 找到 dom 节点
  const currentDOM = findDOMByVNode(VNode);
  // 删除 dom 节点
  if (currentDOM) currentDOM.remove();
}

也就是说,我们应该在 removeVNode 函数中调用 componentWillUnmount 函数

那我们就在这里面调用 componentWillUnmount 函数,完成组件卸载

js 复制代码
function removeVNode(VNode) {
  // 找到 dom 节点
  const currentDOM = findDOMByVNode(VNode);
  // 删除 dom 节点
  if (currentDOM) currentDOM.remove();
  // 如果存在生命周期函数 componentWillUnmount 函数,调用
  if (VNode.classInstance && VNode.classInstance.componentWillUnmount)
    VNode.classInstance.componentWillUnmount();
}

shouldComponentUpdate

shouldComponentUpdate 是在 props 或者 state 变化时,决定是否需要更新组件:

  • 如果函数返回 true,则会继续执行更新操作
  • 如果函数返回 false,则不会执行更新操作
  1. 界面展示不受到 propsstate 的变化的影响的时候使用
  2. 默认行为是返回 true,也就是需要更新
  3. 返回 falserendercomponentDidUpdate 都不会执行
  4. 该函数在 render 函数执行之前调用
  5. 初始化渲染,或者执行 forceUpdate 的时候,不会调用该函数
  6. 仅仅作为性能优化的手段,不建议手动编写,而是使用 PureComponent

通过上面分析,我们知道 shouldComponentUpdate 函数会在 componentDidUpdate 函数之前调用

componentDidUpdate 函数是在 Component.update 函数中调用,再往上一层就是 Updater.launchUpdate 函数调用

通过官方文档得知,shouldComponentUpdate 调用时,propsstate 还没有更新完成,也就是说最新的 propsstate 是通过 shouldComponentUpdate 的参数获取

如何在 launchUpdate 中获取到最新的 propsstate 呢?

nextState 比较好获取,是因为 launchUpdate 本身就是在做 state 合并

这是之前的代码:

js 复制代码
ClassComponentInstance.state = pendingStates.reduce((state, partialState) => {
  return { ...state, ...partialState };
}, ClassComponentInstance.state);

先不直接给实例更新 state

js 复制代码
let nextState = pendingStates.reduce((state, partialState) => {
  return { ...state, ...partialState };
}, ClassComponentInstance.state);

nextProps 哪里来呢?

我们看下代码中在哪里调用了 launchUpdate

一个是在 setState 时调用的,一个是在类组件更新时调用 updateClassComponent

setState 中调用肯定是是拿不到 nextProps,所以只可能在 updateClassComponent 调用时处理 nextProps

js 复制代码
function updateClassComponent(oldVNode, newVNode) {
  // 对于当前界面,旧的实例是与页面上已渲染的组件是相对应的,在生命周期函数中,会尝试比较 newProps 和 oldProps
  const classInstance = (newVNode.classInstance = oldVNode.classInstance);
  classInstance.updater.launchUpdate();
}

nextProps 可以通过 newVNode.props 中获取,因为 props 都是挂载在 VNode 上的

所以代码改造如下

js 复制代码
function updateClassComponent(oldVNode, newVNode) {
  // 对于当前界面,旧的实例是与页面上已渲染的组件是相对应的,在生命周期函数中,会尝试比较 newProps 和 oldProps
  const classInstance = (newVNode.classInstance = oldVNode.classInstance);
  // 将 nextProps 传递给 launchUpdate
  classInstance.updater.launchUpdate(newVNode.props);
}

现在 nextPropsnextState 都有了,调用 shouldComponentUpdate 函数,传入 nextPropsnextState

通过变量 isShouldUpdate 来判断是否需要调用 Component.update 函数

js 复制代码
class Updater {
  launchUpdate(nextProps) {
    // 用来控制是否需要调用 update 方法
    let isShouldUpdate = true;
    // 调用 shouldComponentUpdate 生命周期函数,如果返回 true,将 isShouldUpdate 设置为 false
    if (
      ClassComponentInstance.shouldComponentUpdate &&
      !ClassComponentInstance.shouldComponentUpdate(nextProps, nextState)
    ) {
      isShouldUpdate = false;
    }
    // 如果 isShouldUpdate 为 false,则不用调用 update
    if (isShouldUpdate) {
      // 更新视图,将 preProps 和 preState 传递给 update 函数
      ClassComponentInstance.update(preProps, preState);
    }
  }
}

getDerivedStateFromProps

getDerivedStateFromProps 函数是在 render 函数执行之前调用

主要作用是根据 props 来更新 state

  1. render 函数执行之前调用,同时也在 shouldComponentUpdate 之前调用
  2. 返回一个对象则更新 state,返回 null 表示没有任何更新
  3. 使用这个函数的场景很少,当 state 需要随着 props 的变化而变化的时候才会用到,其实相当于一种缓冲机制
  4. 如果需要使用的时候,可以考虑用 memoization 技术
  5. 静态函数不能访问类实例,因此多个类组件可以抽取为纯函数的公用逻辑
  6. 该函数在初始化挂载,更新,调用 forceUpdate 都会执行,与场景无关,而 UNSAFE_componentWillReceiveProps 只在由于父组件导致的更新的场景下调用,组件内的 setState 导致的更新不会调用

那我们应该如何实现 getDerivedStateFromProps 函数呢?

上面我们实现了 shouldComponentUpdate 函数,getDerivedStateFromProps 函数在 shouldComponentUpdate 之前调用

所以在 shouldComponentUpdate 函数之前调用 getDerivedStateFromProps 函数即可

getDerivedStateFromProps 函数接收两个参数,nextPropsprevState

  • nextProps 是最新的是 launchUpdate 函数中传递过来的
  • prevStateComponent.state,此时的 Component.state 还没有更新
js 复制代码
class Updater {
  launchUpdate(nextProps) {
    // ...
    // 生命周期函数 getDerivedStateFromProps
    // 访问静态方法可以通过 constructor
    if (ClassComponentInstance.constructor.getDerivedStateFromProps) {
      // 拿到合并 props 后的 state
      let newState =
        // 调用 getDerivedStateFromProps 生命周期函数
        ClassComponentInstance.constructor.getDerivedStateFromProps(
          // 传入的 props
          nextProps,
          // 之前的 state
          ClassComponentInstance.state
        );
      // 将 newState 合并到 nextState 中
      nextState = { ...nextState, ...newState };
    }
    // 生命周期函数 shouldComponentUpdate
    // 调用 shouldComponentUpdate 生命周期函数,如果返回 true,将 isShouldUpdate 设置为 false
    if (
      ClassComponentInstance.shouldComponentUpdate &&
      !ClassComponentInstance.shouldComponentUpdate(nextProps, nextState)
    ) {
      isShouldUpdate = false;
    }
  }
}

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate 函数并不常用,仅仅在一些特定 UI 变化的场景才会用到

  1. render 函数执行完成生成真实 DOM 后,DOM 挂载到页面前执行
  2. 使得组件在 DOM 发生变化之前可以获取一些信息
  3. 返回的任何值都会作为 componentDidUpdate 的第三个参数传入

通过上面分析 getSnapshotBeforeUpdate 函数在 render 之前调用,并且它的返回值作为 componentDidUpdate 的第三个参数传入

js 复制代码
class Component {
  update(preProps, preState) {
    // 调用 getSnapShotBeforeUpdate 生命周期函数,传入 preProps 和 preState
    let snapshot =
      this.getSnapshotBeforeUpdate &&
      this.getSnapshotBeforeUpdate(preProps, preState);
    // 调用 render 方法,得到新的 VNode
    let newVNode = this.render();

    // 如果存在生命周期函数 componentDidUpdate 函数,调用,并传入 preProps 和 preState
    if (this.componentDidUpdate)
      // 将 snapshot 作为第三个参数传递给 componentDidUpdate
      this.componentDidUpdate(preProps, preState, snapshot);
  }
}

这个函数实现还是比较简单的

这里要重点讲一下 deepClone 函数

之前我们拿 prevStateprevProps 时,是直接从 Component 中取的,这里最好要做一下深拷贝

js 复制代码
const preState = ClassComponentInstance.state;
const preProps = ClassComponentInstance.props;

做深拷贝,就需要单独写一个深拷贝的函数 deepClone

深拷贝本质就是调用该数据的 toString 方法

先定义一个 getType 函数,用来获取数据 toString 后的类型

js 复制代码
function getType(obj) {
  let typeMap = {
    "[Object Boolean]": "boolean",
    "[Object Number]": "number",
    "[Object String]": "string",
    "[Object Function]": "function",
    "[Object Array]": "array",
    "[Object Date]": "date",
    "[Object RegExp]": "regExp",
    "[Object Undefined]": "undefined",
    "[Object Null]": "null",
    "[Object Object]": "object",
  };
  return typeMap[Object.prototype.toString.call(obj)];
}

这里的深拷贝只考虑了数组和对象,其他的类型可以自己扩展

js 复制代码
const deepClone = (data) => {
  let type = getType(data);
  let resultValue;
  // 如果不是数组或者对象,直接返回
  if (type !== "array" && type !== "object") return data;
  // 对数据进行深拷贝
  if (type === "array") {
    resultValue = [];
    // 遍历数组,递归调用 deepClone
    data.forEach((item) => {
      resultValue.push(deepClone(item));
    });
    return resultValue;
  }
  // 对对象进行深拷贝
  if (type === "object") {
    resultValue = {};
    // 遍历对象,递归调用 deepClone
    for (const key in data) {
      // 只拷贝对象自身的属性,不拷贝原型链上的属性
      if (data.hasOwnProperty(key)) {
        resultValue[key] = deepClone(data[key]);
      }
    }
    return resultValue;
  }
};

总结

6 个生命周期函数:

  1. componentDidMount
    1. 它是在组件挂载到页面上后调用
    2. 可以操作 DOM,也就是说可以使用 document.getElementById(xxx)
    3. 可以进行网络请求
    4. 可以做事件订阅,但需要在 componentWillUnmount 中取消订阅
    5. 不适合在这里调用 setState,会触发一次更新,state 初始值最好在 constructor 中赋值
  2. componentDidUpdate
    1. 更新完成后调用,初始化渲染不会调用
    2. 当组件完成更新,需要对 DOM 进行某种操作的时候,适合在这个函数中进行
    3. props 有所变化时,可以进行一些操作,比如网络请求
    4. 这里虽然可以调用 setState,但这是有条件的调用,否则会陷入死循环
    5. 如果 shouldComponentUpdate 返回 falsecomponentDidUpdate 不会执行
    6. 如果实现了 getSnapshotBeforeUpdatecomponentDidUpdate 会接收第三个参数
  3. componentWillUnmount
    1. 组件从 DOM 树上卸载完成的时候调用该函数
    2. 执行一些清理操作,比如清除定时器,取消事件订阅,取消网络请求等
    3. 不要在这里调用 setState,不会产生任何效果,卸载后不会重新渲染
  4. shouldComponentUpdate
    1. 界面展示不受到 propsstate 的变化的影响的时候使用
    2. 默认行为是返回 true,也就是需要更新
    3. 返回 falserendercomponentDidUpdate 都不会执行
    4. 该函数在 render 函数执行之前调用
    5. 初始化渲染,或者执行 forceUpdate 的时候,不会调用该函数
    6. 仅仅作为性能优化的手段,不建议手动编写,而是使用 PureComponent
  5. getDerivedStateFromProps
    1. render 函数执行之前调用,同时也在 shouldComponentUpdate 之前调用
    2. 返回一个对象则更新 state,返回 null 表示没有任何更新
    3. 使用这个函数的场景很少,当 state 需要随着 props 的变化而变化的时候才会用到,其实相当于一种缓冲机制
    4. 该函数在初始化挂载,更新,调用 forceUpdate 都会执行,与场景无关,而 UNSAFE_componentWillReceiveProps 只在由于父组件导致的更新的场景下调用,组件内的 setState 导致的更新不会调用
  6. getSnapshotBeforeUpdate
    1. render 函数执行完成生成真实 DOM 后,DOM 挂载到页面前执行
    2. 使得组件在 DOM 发生变化之前可以获取一些信息
    3. 返回的任何值都会作为 componentDidUpdate 的第三个参数传入

源码

  1. componentDidMount
  2. componentDidUpdate
  3. componentWillUnmount
  4. shouldComponentUpdate
  5. getDerivedStateFromProps
  6. getSnapshotBeforeUpdate
相关推荐
Cachel wood18 分钟前
python round四舍五入和decimal库精确四舍五入
java·linux·前端·数据库·vue.js·python·前端框架
学代码的小前端19 分钟前
0基础学前端-----CSS DAY9
前端·css
joan_8523 分钟前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
m0_748236111 小时前
Calcite Web 项目常见问题解决方案
开发语言·前端·rust
Watermelo6171 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
m0_748248941 小时前
HTML5系列(11)-- Web 无障碍开发指南
前端·html·html5
m0_748235611 小时前
从零开始学前端之HTML(三)
前端·html
一个处女座的程序猿O(∩_∩)O3 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
hackeroink6 小时前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者8 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart