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 开发趋势!

相关推荐
fhsWar31 分钟前
Vue3 props: `required: true` 与 vant 的`makeRequiredProp`
前端·javascript·vue.js
光影少年34 分钟前
AIGG人工智能生态及学习路线和应用领域
人工智能·学习
递归不收敛37 分钟前
多模态学习大纲笔记(未完成)
人工智能·笔记·学习·自然语言处理
泷羽Sec-静安1 小时前
Less-1 GET-Error based-Single quotes-String GET-基于错误-单引号-字符串
前端·css·网络·sql·安全·web安全·less
小时前端1 小时前
虚拟DOM已死?90%内存节省的Vapor模式正在颠覆前端
前端·html
Keepreal4962 小时前
Web Components简介及如何使用
前端·javascript·html
进击的野人2 小时前
JavaScript变量声明的前世今生:从var到let/const的演进
javascript
jump6802 小时前
TS中 unknown 和 any 的区别
前端
FPGA-李宇航2 小时前
FPGA中,“按键控制LED灯实验”学习中常见问题、解决思路和措施以及经验总结!!!(新手必看)
学习·fpga开发·按键控制led灯
无羡仙2 小时前
AI终于‘看见’网页了!Stagewise让UI修改从‘盲调’变‘指哪打哪
前端