React学习教程,从入门到精通,React 组件生命周期详解(适用于 React 16.3+,推荐函数组件 + Hooks)(17)

React 组件生命周期详解(适用于 React 16.3+,推荐函数组件 + Hooks)

⚠️ 重要说明:自 React 16.3 起,部分生命周期方法被标记为不安全(如 componentWillMount, componentWillReceiveProps, componentWillUpdate),并在 React 17 中被移除。目前推荐使用函数组件 + Hooks(如 useEffect, useLayoutEffect)来管理副作用和生命周期。但为了知识完整性,本文将同时介绍类组件的完整生命周期现代函数组件的等效写法


一、React 类组件生命周期(完整版)

1. 挂载阶段(Mounting)

生命周期方法 说明 是否常用
constructor() 构造函数,初始化 state 和绑定方法
static getDerivedStateFromProps() 静态方法,根据 props 更新 state(不常用) ⚠️
render() 渲染 UI,必须实现
componentDidMount() 组件挂载后调用,适合发起网络请求、订阅事件等

2. 更新阶段(Updating)

生命周期方法 说明 是否常用
static getDerivedStateFromProps() 同上,在 props 更新时也会调用 ⚠️
shouldComponentUpdate() 判断是否需要重新渲染,默认返回 true ✅(优化)
render() 重新渲染
getSnapshotBeforeUpdate() 在 DOM 更新前获取快照(如滚动位置) ⚠️
componentDidUpdate() DOM 更新后调用,可执行副作用操作(如更新 DOM、发请求)

3. 卸载阶段(Unmounting)

生命周期方法 说明 是否常用
componentWillUnmount() 组件卸载前调用,用于清理定时器、取消订阅等

4. 错误处理阶段(Error Handling)

生命周期方法 说明 是否常用
static getDerivedStateFromError() 捕获子组件错误并更新 state 显示降级 UI
componentDidCatch() 捕获错误并记录日志

二、完整类组件生命周期案例(带详细注释)

jsx 复制代码
import React from 'react';

class LifecycleDemo extends React.Component {
  constructor(props) {
    super(props);
    console.log('[constructor] 构造函数:初始化 state 和绑定方法');
    this.state = {
      count: 0,
      error: null
    };
    // 绑定方法(或使用箭头函数)
    this.handleClick = this.handleClick.bind(this);
  }

  // ⚠️ 静态方法:根据 props 派生 state(不推荐滥用)
  static getDerivedStateFromProps(props, state) {
    console.log('[getDerivedStateFromProps] 根据 props 更新 state(谨慎使用)');
    // 示例:如果 props.reset 为 true,则重置 count
    if (props.reset && state.count !== 0) {
      return { count: 0 };
    }
    return null; // 返回 null 表示不更新 state
  }

  // ✅ 组件挂载后:适合发起网络请求、订阅事件
  componentDidMount() {
    console.log('[componentDidMount] 组件已挂载到 DOM,可执行副作用操作');
    // 模拟网络请求
    this.timer = setInterval(() => {
      console.log('定时器运行中...');
    }, 3000);
    // 模拟聚焦
    this.inputRef && this.inputRef.focus();
  }

  // ✅ 性能优化:判断是否需要重新渲染
  shouldComponentUpdate(nextProps, nextState) {
    console.log('[shouldComponentUpdate] 判断是否需要更新,避免不必要的渲染');
    // 示例:只有当 count 改变时才更新
    if (this.state.count === nextState.count) {
      return false; // 不更新
    }
    return true; // 更新
  }

  // ⚠️ DOM 更新前获取快照(如滚动位置)
  getSnapshotBeforeUpdate(prevProps, prevState) {
    console.log('[getSnapshotBeforeUpdate] DOM 更新前获取快照');
    // 示例:获取滚动位置
    if (prevState.count !== this.state.count) {
      const scrollPos = window.scrollY;
      return scrollPos; // 返回值会作为第三个参数传给 componentDidUpdate
    }
    return null;
  }

  // ✅ DOM 更新后:可操作 DOM 或发起请求
  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('[componentDidUpdate] DOM 已更新,可执行副作用');
    if (snapshot !== null) {
      console.log('更新前的滚动位置:', snapshot);
    }
    // 示例:如果 count 改变,发送数据到服务器
    if (prevState.count !== this.state.count) {
      console.log(`计数更新为:${this.state.count}`);
    }
  }

  // ✅ 组件卸载前:清理定时器、取消订阅
  componentWillUnmount() {
    console.log('[componentWillUnmount] 组件即将卸载,清理副作用');
    if (this.timer) {
      clearInterval(this.timer);
      console.log('定时器已清理');
    }
  }

  // ✅ 错误边界:捕获子组件错误
  static getDerivedStateFromError(error) {
    console.log('[getDerivedStateFromError] 捕获子组件错误,更新 state 显示降级 UI');
    return { error: error.toString() };
  }

  // ✅ 错误边界:记录错误日志
  componentDidCatch(error, errorInfo) {
    console.log('[componentDidCatch] 捕获错误并记录日志');
    console.error('错误信息:', error);
    console.error('错误堆栈:', errorInfo.componentStack);
    // 可以发送错误报告到服务器
  }

  // 事件处理函数
  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }

  // 渲染函数(必须实现)
  render() {
    console.log('[render] 渲染 UI');
    if (this.state.error) {
      return <h2>发生错误:{this.state.error}</h2>;
    }

    return (
      <div style={{ padding: '20px', border: '1px solid #ccc' }}>
        <h2>React 类组件生命周期演示</h2>
        <p>当前计数:{this.state.count}</p>
        <button onClick={this.handleClick}>点击 +1</button>
        <br /><br />
        <input 
          ref={el => this.inputRef = el} 
          placeholder="自动聚焦的输入框" 
          style={{ marginTop: '10px' }}
        />
        {/* 故意制造错误 */}
        {/* <button onClick={() => { throw new Error('故意制造的错误!'); }}>
          点击制造错误
        </button> */}
      </div>
    );
  }
}

export default LifecycleDemo;

三、现代 React:函数组件 + Hooks 生命周期等效写法

Hooks 对应关系:

类组件生命周期 函数组件 Hooks 等效写法
constructor useState, useRef 初始化
componentDidMount useEffect(() => {}, [])
componentDidUpdate useEffect(() => {})(依赖变化时)
componentWillUnmount useEffect(() => { return () => {} }, [])
shouldComponentUpdate React.memo, useMemo, useCallback
错误边界 仍需类组件(Hooks 无直接等效)

函数组件完整案例:

jsx 复制代码
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';

function HooksLifecycleDemo({ reset }) {
  const [count, setCount] = useState(0);
  const [error, setError] = useState(null);
  const inputRef = useRef(null);
  const timerRef = useRef(null);

  // ✅ 等效于 componentDidMount + componentDidUpdate + componentWillUnmount
  useEffect(() => {
    console.log('[useEffect] 组件挂载或 reset 变化时执行');

    // 模拟 componentDidMount
    timerRef.current = setInterval(() => {
      console.log('定时器运行中...');
    }, 3000);

    // 聚焦输入框
    if (inputRef.current) {
      inputRef.current.focus();
    }

    // ✅ 等效于 componentWillUnmount:返回清理函数
    return () => {
      console.log('[useEffect cleanup] 组件卸载或 reset 变化前清理');
      if (timerRef.current) {
        clearInterval(timerRef.current);
        console.log('定时器已清理');
      }
    };
  }, [reset]); // 依赖数组:仅在 reset 变化或首次挂载时执行

  // ✅ 等效于 componentDidUpdate(监听 count 变化)
  useEffect(() => {
    console.log('[useEffect count] count 发生变化:', count);
    // 可以在这里发送数据到服务器
    if (count > 0) {
      console.log(`计数更新为:${count}`);
    }
  }, [count]); // 仅当 count 变化时执行

  // ✅ 等效于 shouldComponentUpdate - 使用 useMemo 优化计算
  const expensiveValue = useMemo(() => {
    console.log('[useMemo] 计算昂贵值(仅在 count 变化时重新计算)');
    return count * 1000;
  }, [count]);

  // ✅ 等效于 shouldComponentUpdate - 使用 useCallback 缓存函数
  const handleClick = useCallback(() => {
    setCount(prev => prev + 1);
  }, []); // 依赖为空数组,函数不会重新创建

  // 渲染函数
  if (error) {
    return <h2>发生错误:{error}</h2>;
  }

  return (
    <div style={{ padding: '20px', border: '1px solid #ccc', marginTop: '20px' }}>
      <h2>React 函数组件 + Hooks 生命周期演示</h2>
      <p>当前计数:{count}</p>
      <p>昂贵计算值:{expensiveValue}</p>
      <button onClick={handleClick}>点击 +1</button>
      <br /><br />
      <input 
        ref={inputRef} 
        placeholder="自动聚焦的输入框" 
        style={{ marginTop: '10px' }}
      />
    </div>
  );
}

// 使用示例(父组件)
function App() {
  const [resetFlag, setResetFlag] = useState(false);

  return (
    <div>
      <h1>React 生命周期演示</h1>
      <button onClick={() => setResetFlag(!resetFlag)}>
        切换 Reset ({resetFlag ? 'true' : 'false'})
      </button>
      <hr />
      <LifecycleDemo reset={resetFlag} />
      <hr />
      <HooksLifecycleDemo reset={resetFlag} />
    </div>
  );
}

export default App;

四、生命周期执行顺序总结

类组件挂载顺序:

复制代码
constructor → getDerivedStateFromProps → render → componentDidMount

类组件更新顺序(props 或 state 变化):

复制代码
getDerivedStateFromProps → shouldComponentUpdate → render → getSnapshotBeforeUpdate → componentDidUpdate

类组件卸载顺序:

复制代码
componentWillUnmount

函数组件执行顺序:

  • 初始化:useStateuseMemouseCallbackrenderuseEffect(依赖为空)
  • 更新:renderuseEffect(依赖变化)→ 清理上一次 effect → 执行新的 effect
  • 卸载:清理所有 effect

五、最佳实践建议

  1. 优先使用函数组件 + Hooks:更简洁、易测试、逻辑复用方便。
  2. 避免使用 getDerivedStateFromProps:除非必要,否则容易导致 bug。
  3. 合理使用 useEffect 依赖数组:避免无限循环或遗漏依赖。
  4. 及时清理副作用 :在 useEffect 返回函数中清理定时器、订阅等。
  5. 性能优化 :使用 React.memo, useMemo, useCallback 避免不必要的渲染。
  6. 错误边界:对于可能出错的组件,用类组件包裹提供降级 UI。

六、常见问题

Q:为什么我的 useEffect 执行了两次?

A:在开发模式下,React 会故意卸载并重新挂载组件以帮助发现清理问题。生产环境不会。

Q:如何模拟 shouldComponentUpdate?

A:使用 React.memo 包裹组件,或使用 useMemo/useCallback 优化子组件。

Q:Hooks 能完全替代类组件吗?

A:几乎可以,除了错误边界目前仍需类组件实现。


通过以上完整案例和注释,你应该对 React 生命周期有了全面理解。在实际开发中,推荐使用函数组件 + Hooks 方案,它更符合现代 React 开发趋势!

相关推荐
面向星辰2 小时前
html音视频和超链接标签,颜色标签
前端·html·音视频
lxh01132 小时前
LRU 缓存
开发语言·前端·javascript
yangzhi_emo3 小时前
ES6笔记5
前端·笔记·es6
wow_DG3 小时前
【Vue2 ✨】Vue2 入门之旅 · 进阶篇(二):虚拟 DOM 与 Diff 算法
开发语言·javascript·vue.js·算法·前端框架
Hexene...3 小时前
【前端Vue】el-dialog关闭后黑色遮罩依然存在如何解决?
前端·javascript·vue.js·elementui·前端框架
Jay_See3 小时前
JC链客云——项目过程中获得的知识、遇到的问题及解决
前端·javascript·vue.js
yuxb734 小时前
Docker 学习笔记(七):Docker Swarm 服务管理与 Containerd 实践
笔记·学习·docker
普通码农4 小时前
Element Plus 数字输入框箭头隐藏方案
前端