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

发布于:2025-09-13 ⋅ 阅读:(18) ⋅ 点赞:(0)

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() 捕获错误并记录日志

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

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 无直接等效)

函数组件完整案例:

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