React生命周期

下面,我们来系统的梳理关于 类组件生命周期 的基本知识点:


一、基础概念

1.1 什么是生命周期?

生命周期(Lifecycle)指的是组件从创建到销毁的整个过程。React类组件提供了一系列生命周期方法(也称为生命周期钩子),允许开发者在组件的不同阶段执行自定义代码。

1.2 生命周期的三个阶段

挂载阶段 更新阶段 卸载阶段

  1. 挂载阶段(Mounting):组件被创建并插入DOM
  2. 更新阶段(Updating):组件因props或state变化而重新渲染
  3. 卸载阶段(Unmounting):组件从DOM中移除

二、生命周期方法详解

2.1 挂载阶段(Mounting)

2.1.1 constructor()
  • 调用时机:组件初始化时调用
  • 主要用途
    • 初始化state
    • 绑定事件处理方法
  • 注意事项
    • 必须首先调用super(props)
    • 避免在此方法中引入副作用
jsx 复制代码
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
  }
  
  // ...
}
2.1.2 static getDerivedStateFromProps()
  • 调用时机:在调用render方法之前调用,并且在初始挂载及后续更新时都会被调用
  • 主要用途:使组件在props变化时更新state
  • 注意事项
    • 应返回一个对象来更新state,如果返回null则不更新
    • 此方法无权访问组件实例
jsx 复制代码
class ScrollPosition extends React.Component {
  state = { scrollY: 0 };
  
  static getDerivedStateFromProps(props, state) {
    if (props.resetPosition) {
      return { scrollY: 0 };
    }
    return null;
  }
  
  // ...
}
2.1.3 render()
  • 调用时机:必须实现的方法,负责返回JSX
  • 主要用途:渲染组件UI
  • 注意事项
    • 应为纯函数,不修改组件状态
    • 不直接与浏览器交互
jsx 复制代码
class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}
2.1.4 componentDidMount()
  • 调用时机:组件挂载后(插入DOM树中)立即调用
  • 主要用途
    • 执行DOM操作
    • 发起网络请求
    • 设置订阅
  • 注意事项:此方法中可以调用setState,但会触发额外渲染
jsx 复制代码
class DataFetcher extends React.Component {
  componentDidMount() {
    fetch(this.props.url)
      .then(res => res.json())
      .then(data => this.setState({ data }));
  }
  
  // ...
}

2.2 更新阶段(Updating)

2.2.1 static getDerivedStateFromProps()
  • 同挂载阶段,在每次更新前也会被调用
2.2.2 shouldComponentUpdate()
  • 调用时机:在重新渲染前触发
  • 主要用途:通过返回true/false控制组件是否更新
  • 注意事项
    • 默认返回true
    • 用于性能优化,避免不必要的渲染
    • 不建议深层比较
jsx 复制代码
class PureComponentLike extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 仅当count变化时才更新
    return nextState.count !== this.state.count;
  }
  
  // ...
}
2.2.3 render()
  • 同挂载阶段,负责渲染更新
2.2.4 getSnapshotBeforeUpdate()
  • 调用时机:在最近一次渲染输出(提交到DOM节点)之前调用
  • 主要用途:捕获渲染前的DOM信息(如滚动位置)
  • 注意事项
    • 返回值将作为参数传递给componentDidUpdate()
    • 此方法不常用
jsx 复制代码
class ScrollList extends React.Component {
  getSnapshotBeforeUpdate(prevProps, prevState) {
    if (prevProps.items.length < this.props.items.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }
  
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }
  
  // ...
}
2.2.5 componentDidUpdate()
  • 调用时机:更新完成后立即调用(首次渲染不会调用)
  • 主要用途
    • 执行DOM操作
    • 进行网络请求(需比较props)
  • 注意事项
    • 可以调用setState,但必须包裹在条件语句中
    • 避免无限循环
jsx 复制代码
class DataUpdater extends React.Component {
  componentDidUpdate(prevProps) {
    if (this.props.id !== prevProps.id) {
      fetch(`/data/${this.props.id}`)
        .then(res => res.json())
        .then(data => this.setState({ data }));
    }
  }
  
  // ...
}

2.3 卸载阶段(Unmounting)

2.3.1 componentWillUnmount()
  • 调用时机:组件卸载及销毁之前调用
  • 主要用途
    • 清理定时器
    • 取消网络请求
    • 移除事件监听
    • 清理订阅
  • 注意事项:不应调用setState()
jsx 复制代码
class TimerComponent extends React.Component {
  componentDidMount() {
    this.timerID = setInterval(() => this.tick(), 1000);
  }
  
  componentWillUnmount() {
    clearInterval(this.timerID);
  }
  
  // ...
}

2.4 错误处理

2.4.1 static getDerivedStateFromError()
  • 调用时机:后代组件抛出错误后调用
  • 主要用途:返回一个值以更新state,用于渲染降级UI
  • 注意事项
    • 在渲染阶段调用,不允许执行副作用
    • 仅用于捕获渲染/生命周期错误
jsx 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>发生错误,请重试</h1>;
    }
    return this.props.children;
  }
}
2.4.2 componentDidCatch()
  • 调用时机:后代组件抛出错误后调用
  • 主要用途
    • 记录错误信息
    • 执行副作用操作
  • 注意事项
    • 在提交阶段调用,可以执行副作用
    • 仅用于捕获渲染/生命周期错误
jsx 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  componentDidCatch(error, info) {
    logErrorToService(error, info);
    this.setState({ hasError: true });
  }
  
  render() {
    if (this.state.hasError) {
      return <h1>发生错误,请重试</h1>;
    }
    return this.props.children;
  }
}

三、生命周期执行顺序

3.1 挂载过程

Constructor getDerivedStateFromProps Render componentDidMount 初始化props/state 计算新state 渲染DOM 可触发setState Constructor getDerivedStateFromProps Render componentDidMount

3.2 更新过程

getDerivedStateFromProps shouldComponentUpdate Render getSnapshotBeforeUpdate componentDidUpdate 接收新props/state 返回true则继续 渲染UI 传递快照 可触发setState getDerivedStateFromProps shouldComponentUpdate Render getSnapshotBeforeUpdate componentDidUpdate

3.3 卸载过程

componentWillUnmount 执行清理操作 componentWillUnmount

四、生命周期最佳实践

4.1 数据获取

  • 使用componentDidMount进行初始数据获取
  • 使用componentDidUpdate进行基于props变化的数据更新

4.2 性能优化

  • 使用shouldComponentUpdate避免不必要的渲染
  • 使用React.PureComponent自动浅比较props和state

4.3 资源清理

  • componentWillUnmount中清除所有副作用资源

4.4 错误处理

  • 使用错误边界(Error Boundaries)捕获组件树错误

五、生命周期与现代React

5.1 生命周期方法与Hooks对比

生命周期方法 Hook 替代方案
constructor useState 初始化
componentDidMount useEffect(() => {}, [])
componentDidUpdate useEffect(() => {}, [deps])
componentWillUnmount useEffect 返回清理函数
shouldComponentUpdate React.memo

5.2 废弃的生命周期方法

React 17+ 已废弃以下方法:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

替代方案:

  • 使用static getDerivedStateFromProps替代componentWillReceiveProps
  • 使用getSnapshotBeforeUpdate替代componentWillUpdate

六、实战案例

6.1 数据获取组件

jsx 复制代码
class DataLoader extends React.Component {
  state = {
    data: null,
    isLoading: true,
    error: null
  };

  componentDidMount() {
    this.fetchData();
  }

  componentDidUpdate(prevProps) {
    if (prevProps.url !== this.props.url) {
      this.fetchData();
    }
  }

  componentWillUnmount() {
    // 如果请求未完成,可以在这里中止
  }

  async fetchData() {
    this.setState({ isLoading: true, error: null });
    
    try {
      const response = await fetch(this.props.url);
      const data = await response.json();
      this.setState({ data, isLoading: false });
    } catch (error) {
      this.setState({ error, isLoading: false });
    }
  }

  render() {
    const { data, isLoading, error } = this.state;
    
    if (isLoading) return <Spinner />;
    if (error) return <ErrorDisplay error={error} />;
    return <DataView data={data} />;
  }
}

6.2 滚动位置保持组件

jsx 复制代码
class ScrollPreserver extends React.Component {
  listRef = React.createRef();
  
  getSnapshotBeforeUpdate(prevProps) {
    if (prevProps.items.length < this.props.items.length) {
      const list = this.listRef.current;
      return list.scrollHeight - list.scrollTop;
    }
    return null;
  }
  
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot !== null) {
      const list = this.listRef.current;
      list.scrollTop = list.scrollHeight - snapshot;
    }
  }
  
  render() {
    return (
      <div ref={this.listRef} style={{ overflowY: 'scroll', height: 300 }}>
        {this.props.items.map(item => (
          <div key={item.id}>{item.text}</div>
        ))}
      </div>
    );
  }
}

6.3 错误边界组件

jsx 复制代码
class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null, errorInfo: null };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    this.setState({ error, errorInfo });
    logErrorToService(error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div className="error-boundary">
          <h2>应用发生错误</h2>
          <details style={{ whiteSpace: 'pre-wrap' }}>
            {this.state.error && this.state.error.toString()}
            <br />
            {this.state.errorInfo.componentStack}
          </details>
          <button onClick={() => window.location.reload()}>重新加载</button>
        </div>
      );
    }
    return this.props.children;
  }
}

七、常见问题与解决方案

7.1 避免内存泄漏

问题:组件卸载后调用setState

jsx 复制代码
componentDidMount() {
  this.timer = setInterval(() => {
    this.setState({ time: Date.now() });
  }, 1000);
}

componentWillUnmount() {
  clearInterval(this.timer);
}

7.2 避免无限循环

问题:在componentDidUpdate中无条件调用setState

jsx 复制代码
componentDidUpdate(prevProps) {
  // 错误:无条件调用setState
  // this.setState({ count: this.state.count + 1 });
  
  // 正确:添加条件判断
  if (this.props.id !== prevProps.id) {
    this.fetchData();
  }
}

7.3 正确处理异步操作

问题:组件卸载后处理异步回调

jsx 复制代码
componentDidMount() {
  this._isMounted = true;
  fetchData().then(data => {
    if (this._isMounted) {
      this.setState({ data });
    }
  });
}

componentWillUnmount() {
  this._isMounted = false;
}

八、生命周期图解总结

错误处理 卸载阶段 更新阶段 挂载阶段 true false getDerivedStateFromError componentDidCatch componentWillUnmount componentDidUpdate getDerivedStateFromProps componentDidMount constructor getDerivedStateFromProps render shouldComponentUpdate render getSnapshotBeforeUpdate 跳过渲染 render

总结

  1. 理解三大阶段:挂载、更新、卸载
  2. 掌握核心方法
    • constructor:初始化状态和绑定方法
    • componentDidMount:执行副作用操作
    • shouldComponentUpdate:优化性能
    • componentDidUpdate:响应更新
    • componentWillUnmount:清理资源
  3. 使用错误边界:捕获组件树中的错误
  4. 遵循最佳实践
    • 避免在render中执行副作用
    • 在componentWillUnmount中清理资源
    • 使用条件判断防止无限循环
  5. 与现代React结合
    • 了解生命周期方法与Hooks的对应关系
    • 避免使用已废弃的生命周期方法