React生命周期方法

219 阅读12分钟

什么是生命周期方法

React组件从创建到销毁的整个过程,它分为三个阶段:挂载更新卸载。这个过程就像人的生命过程,出生(挂载)成长/变化(更新)死亡(卸载),因此被称为生命周期。生命周期方法就是React组件在不同生命周期触发的方法,通过这些方法控制组件的行为和功能。

为什么需要生命周期方法

先思考一个问题,假如没有声明周期方法,那么会发生什么事情?

  • 不知道在合适请求服务器的数据。
  • 不知道在什么时候设置定时器或者清理定时器。
  • 不知道怎么在组件更新后去更新DOM。
  • 不知道何时获取DOM并操作DOM。
  • ......

你会发现假如没有生命周期方法,无法在控制组件的行为和功能,就像React Hooks之前的函数式组件,只能用于渲染。

因此需要生命周期方法帮助我们在合适的时机控制组件的行为和功能。

生命周期方法有哪些以及作用是什么

虽然React组件从创建到销毁只有挂载更新卸载三个阶段,但是不意味着只有三个生命周期方法,每个阶段都有不同的声明周期方法用于不同的功能。

1.挂载(Mounting)

当组件被创建并插入DOM中时,以下方法会被调用:

constructor(props)

构造函数用于初始化组件的状态和绑定事件处理函数。在构造函数中,我们可以通过this.state来设置初始状态,通过this.props来访问传入的属性。注意:如果使用了构造函数,必须调用super(props)来确保this的上下文正确

static getDerivedStateFromProps(nextProps, prevState)

在组件的propsstate发生变化后调用。可以返回一个对象以更新状态,或者返回null不做任何更改。用于处理从父组件传递的props变化。

render()

返回要渲染的JSX元素。这个方法是必需的,用于描述组件的结构和UI

componentDidMount()

组件挂载后调用。在这里可以进行数据获取、订阅或其他任何需要在组件初始化完成后执行的操作。

2.更新(Updating)

组件在接收到新的 props 或 state 更新时,会经历更新阶段。以下方法会被调用:

static getDerivedStateFromProps(nextProps, prevState)

在组件的propsstate发生变化后调用。可以返回一个对象以更新状态,或者返回null不做任何更改。用于处理从父组件传递的props变化。

shouldComponentUpdate(nextProps, nextState)

控制组件是否重新渲染。返回true表示组件可以更新,返回false表示不更新。用于优化性能。

render()

返回要渲染的JSX元素。这个方法是必需的,用于描述组件的结构和UI

getSnapshotBeforeUpdate(prevProps, prevState)

在最新的渲染输出提交给DOM之前调用。它可以返回一些信息,用于在componentDidUpdate中处理DOM状态

componentDidUpdate(prevProps, prevState, snapshot)

组件更新后调用,可以在这里处理基于更新的副作用,例如发送网络请求或更新DOM

3.卸载(Unmounting)

当组件从DOM中移除时,以下方法会被调用:

componentWillUnmount()

在组件卸载之前调用。可以在这里清理定时器、取消订阅或其他任何清理操作。

4.错误处理(Error Handling)

错误处理在React中主要属于更新(Updating)阶段,但也可以视为独立的处理机制。具体来说,当一个组件的子组件发生错误时,错误处理方法会被调用。对于处理渲染过程中的错误,React 也提供了以下生命周期方法:

static getDerivedStateFromError(error)

在子组件抛出错误时调用。可以返回一个对象来更新状态,从而使下一个渲染显示出错的元素。

componentDidCatch(error, info)

用于记录错误信息。可以捕获子组件的错误信息,并进行日志记录或其他错误处理。

为什么React Hooks不再使用生命周期方法

React Hooks的引入旨在简化函数组件的状态和副作用管理,因此在设计时不再依赖于传统的生命周期方法。以下是一些原因,解释了为什么React Hooks实现时不再考虑使用生命周期方法:

  1. 简化与整合
  • 统一处理副作用:使用useEffect,React将传统生命周期方法(如 componentDidMountcomponentDidUpdatecomponentWillUnmount)的功能整合为一个单一的 API。这种整合简化了组件的结构,减少了逻辑的分散。
  • 清晰的生命周期管理:通过传递依赖数组,开发者可以更直观地控制何时触发副作用,从而使得代码更易于理解。
  1. 避免复杂性与错误
  • 减少逻辑混乱:在类组件中,多个生命周期方法可能需要访问和更新状态。这使得理清不同方法的逻辑关系变得复杂。而Hooks的设计简化了这一问题,使得代码更容易理解和维护。
  1. 更强的复用性
  • 自定义Hooks:React Hooks允许开发者创建自定义Hooks,从而实现状态和副作用逻辑的复用,而不需要依赖HOCrender props。这种方式使得代码更简洁且符合DRY(Don't Repeat Yourself)原则。
  1. 声明式API
  • 更声明式的编程风格:通过Hooks,开发者可以编写更声明式的代码。例如useEffect的使用方式让副作用的意图更加明确,使组件的行为更易于预测。
  1. 提高可组合性
  • 逻辑的组合:Hooks支持将状态逻辑分解成更小的函数,允许将它们组合成复杂的逻辑。这种方式显著提高了组件之间逻辑的重用性和组合性。
  1. 一致性与简洁性
  • 统一的API:Hooks为函数组件提供了一种一致的方式来管理状态与副作用,从而避免了在类组件和函数组件之间的 API 不一致性问题。

总结

React Hooks使用一种新的方式代替生命周期方法控制组件的行为和功能,这种方式更加高效和简洁,因此不再需要生命周期方法。

生命周期方法的注意事项

不安全的周期方法

一些生命周期方法因为在使用时可能导致不一致的行为和复杂的状态管理,在 React 16.3 中被标记为“不安全”,这些方法包括UNSAFE_componentWillMount、UNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate虽然还能使用这些方法,但是建议尽量不使用

1.不一致的行为

UNSAFE_componentWillMount

由于componentWillMount在组件挂载之前被调用,如果在这个生命周期中执行AJAX请求或其他异步操作,并且组件可能在请求完成之前就挂载,那么可能导致状态更新在不合适的时间发生,从而引发错误或异常行为。

UNSAFE_componentWillReceiveProps

该方法在接收到新的props时调用,但如果在该方法中更新状态,可能会导致不必要的复杂性和在不同生命周期间的状态不一致。这使得预测组件的行为变得更加困难。

UNSAFE_componentWillUpdate

这个方法在组件即将更新时被调用,但如果开发者在这个方法中做了副作用的操作(例如,网络请求),可能造成组件更新和渲染状态的不一致。

2. 复杂和不必要的副作用

在这三个生命周期方法中实现副作用将其返回的不可靠性引入了组件。这种设计形式容易导致副作用没有被正确管理,同时也使组件的行为更加复杂和难以理解。

3. 新的替代方案

为了解决这些问题,React 团队引入了getDerivedStateFromPropsgetSnapshotBeforeUpdate等新方法。这些方法提供了更清晰和一致的方式来处理状态管理和副作用:

  • getDerivedStateFromProps:通过返回一个新的状态对象来直接影响组件的状态,更加规范和透明。
  • getSnapshotBeforeUpdate:允许在 DOM 更新前记录一些相关 "快照" 信息,使得处理更新后渲染更加灵活。

4. 提升性能和可维护性

删除这些不安全的生命周期方法使得组件更加可预测,从而提升组件的稳定性和性能。开发者能够更容易地理解组件的操作流程,从而提高维护性。

未被React Hooks代替或者实现的生命周期方法

1.UNSAFE_componentWillMount

  • 没有直接替代:UNSAFE_componentWillMount在组件挂载前调用,用于设置状态或执行副作用。使用此方法在新版React中被认为是不推荐的,因为它可能导致不一致的渲染结果。
  • 替代方案:没有直接替代的Hooks,通常在useEffect中处理初始化逻辑。

2.UNSAFE_componentWillReceiveProps

  • 没有直接替代:此方法在组件接收到新的props之前调用,通常用于更新状态。这个生命周期方法在使用时会导致一些困惑,因此被弃用。
  • 替代方案:你可以在useEffect中使用依赖项来响应props的变化。通过列出props作为依赖useEffect会在这些props变化后执行相应逻辑。

3.UNSAFE_componentWillUpdate

  • 没有直接替代:该方法用于控制组件是否重新渲染,返回truefalse
  • 替代方案:可以通过使用React.memo对函数组件进行优化,或者在类组件中手动实现优化。如果使用useMemouseCallback,也可以在一定程度上控件性能。

4.getSnapshotBeforeUpdate

  • 没有直接替代:此方法在更新之前调用,用于获取DOM状态以便在componentDidUpdate中使用。
  • 替代方案:没有直接对应的Hook,可以使用useRef等来捕获DOM状态。

正确选择生命周期方法

选择合适的生命周期方法: 理解每个生命周期方法的作用,例如:

  • componentDidMount中进行数据获取,而不是在constructor中。
  • componentWillUnmount中进行清理,例如取消订阅或清除定时器。

注意异步操作

处理异步操作: 如果你在生命周期方法中进行异步操作(如网络请求),务必处理组件的卸载状态。例如,不要在组件卸载后调用 setState,这会导致内存泄漏和错误。

componentDidMount() {
  this.fetchData().then(data => {
    if (this._isMounted) {
      this.setState({ data });
    }
  });
}

componentWillUnmount() {
  this._isMounted = false; // 设置状态标识
}

避免过度依赖getDerivedStateFromProps

getDerivedStateFromProps可以根据props更新状态,但不应过度依赖。它应该尽量简化,避免逻辑复杂的状态计算。

使用shouldComponentUpdate优化性能

在适当的情况下使用shouldComponentUpdate来避免不必要的重新渲染。例如,比较新的和旧的propsstate

shouldComponentUpdate(nextProps, nextState) {
  return nextProps.value !== this.props.value; // 只在特定条件下更新
}

避免内存泄漏

在 componentWillUnmount 中清理所有副作用,这包括:

  • 清除定时器
  • 取消网络请求
  • 解除事件监听
componentWillUnmount() {
  clearInterval(this.timer);
  window.removeEventListener('resize', this.handleResize);
}

错误处理

使用componentDidCatchgetDerivedStateFromError处理子组件中的错误。确保为用户提供友好的界面而不是让应用崩溃。

性能考虑

尽量避免在render方法中进行逻辑运算,保持render方法尽可能干净。可以在生命周期方法中进行数据处理或状态更新,以保持渲染的高效性。

注意类组件与函数组件的区别

一旦决定使用Hooks,你将不再使用类组件的生命周期方法。针对函数组件与类组件的不同特性和 API进行设计与开发。

生命周期方法的使用场景

获取服务器数据

import React from 'react';

class DataFetchingComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null,
      loading: true,
      error: null,
    };
    this._isMounted = false; // 标志:组件是否已挂载
  }

  componentDidMount() {
    this._isMounted = true; // 组件已挂载

    // 从服务器获取数据
    fetch('https://jsonplaceholder.typicode.com/posts') // 使用一个公共API获取假数据
      .then(response => {
        if (!response.ok) {
          throw new Error('Network response was not ok');
        }
        return response.json();
      })
      .then(data => {
        if (this._isMounted) { // 只在组件仍然挂载时更新状态
          this.setState({ data, loading: false });
        }
      })
      .catch(error => {
        if (this._isMounted) { // 只在组件仍然挂载时更新状态
          this.setState({ error, loading: false });
        }
      });
  }

  componentWillUnmount() {
    this._isMounted = false; // 组件即将卸载,设置标志
  }

  render() {
    const { data, loading, error } = this.state;

    // 显示加载状态
    if (loading) {
      return <div>Loading...</div>;
    }

    // 处理错误
    if (error) {
      return <div>Error: {error.message}</div>;
    }

    // 数据加载完毕,显示数据
    return (
      <div>
        <h1>Fetched Data from Server:</h1>
        <ul>
          {data.map(item => (
            <li key={item.id}>{item.title}</li>
          ))}
        </ul>
      </div>
    );
  }
}

export default DataFetchingComponent;

订阅和销毁第三方事件

import React from 'react';

class CounterWithLifecycle extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    this.intervalId = null; // 存储定时器ID
  }

  // 组件挂载后设置定时器
  componentDidMount() {
    console.log('Component mounted. Setting interval.');
    this.startTimer();
  }

  // 每次更新时(例如,当组件重渲染时)都会调用
  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      console.log(`Count updated: ${this.state.count}`);
    }
  }

  // 组件卸载前清理定时器
  componentWillUnmount() {
    console.log('Component unmounted. Clearing interval.');
    clearInterval(this.intervalId);
  }

  // 启动计时器函数
  startTimer = () => {
    this.intervalId = setInterval(() => {
      this.setState(prevState => ({
        count: prevState.count + 1,
      }));
    }, 1000);
  };

  // 手动重置计数
  resetCount = () => {
    this.setState({ count: 0 });
  };

  render() {
    return (
      <div>
        <h1>Counter: {this.state.count}</h1>
        <button onClick={this.resetCount}>Reset Count</button>
      </div>
    );
  }
}

export default CounterWithLifecycle;

控制和操作DOM

import React from 'react';

class ResizableBoxCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    this.boxRef = React.createRef(); // 存储对方块的引用
  }

  // 在组件挂载后更新方块大小
  componentDidMount() {
    this.updateBoxSize();
  }

  // 在组件更新后更新方块大小
  componentDidUpdate(prevProps, prevState) {
    // 如果 count 改变,更新方块大小
    if (prevState.count !== this.state.count) {
      this.updateBoxSize();
    }
  }

  // 组件卸载前的清理工作(如有)
  componentWillUnmount() {
    // 此处可以进行必要的清理(本例中不需要)
  }

  // 更新方块大小的方法
  updateBoxSize = () => {
    const box = this.boxRef.current;
    if (box) {
      box.style.width = `${this.state.count * 10}px`; // 每增加 1,宽度增加 10px
    }
  };

  // 增加计数
  increaseCount = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };

  // 减少计数
  decreaseCount = () => {
    this.setState(prevState => ({ count: prevState.count - 1 }));
  };

  // 重置计数
  resetCount = () => {
    this.setState({ count: 0 });
  };

  render() {
    return (
      <div>
        <h1>Count: {this.state.count}</h1>
        <button onClick={this.increaseCount}>Increase</button>
        <button onClick={this.decreaseCount}>Decrease</button>
        <button onClick={this.resetCount}>Reset</button>
        <div
          ref={this.boxRef}
          style={{
            height: '100px',
            width: '0px', // 初始宽度为0
            backgroundColor: 'skyblue',
            transition: 'width 0.5s', // 增加平滑过渡效果
            marginTop: '20px',
          }}
        />
      </div>
    );
  }
}

export default ResizableBoxCounter;

使用getDerivedStateFromProps监听Props和State更新

import React from 'react';

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: props.initialCount, // 使用 props 初始化状态
    };
  }

  static getDerivedStateFromProps(nextProps, prevState) {
    // 如果下一个 props 的 count 与之前的 state 中的 count 不同
    if (nextProps.initialCount !== prevState.count) {
      return { count: nextProps.initialCount }; // 返回新的 state
    }
    return null; // 返回 null 表示不需要更新状态
  }

  increment = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };

  render() {
    return (
      <div>
        <h1>Count: {this.state.count}</h1>
        <button onClick={this.increment}>Increment</button>
      </div>
    );
  }
}

export default Counter;

优化组件渲染

import React from 'react';

class OptimizedCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
      inputValue: '',
    };
  }

  // 控制组件更新的生命周期方法
  shouldComponentUpdate(nextProps, nextState) {
    // 只有在 count 发生变化时才更新组件
    return nextState.count !== this.state.count || nextState.inputValue !== this.state.inputValue;
  }

  // 增加计数
  increaseCount = () => {
    this.setState(prevState => ({ count: prevState.count + 1 }));
  };

  // 输入框内容变化
  handleInputChange = (event) => {
    this.setState({ inputValue: event.target.value });
  };

  render() {
    console.log('Rendering OptimizedCounter');
    return (
      <div>
        <h1>Count: {this.state.count}</h1>
        <button onClick={this.increaseCount}>Increase Count</button>
        <hr />
        <input
          type="text"
          placeholder="Type something..."
          value={this.state.inputValue}
          onChange={this.handleInputChange}
        />
        <p>You typed: {this.state.inputValue}</p>
      </div>
    );
  }
}

export default OptimizedCounter;

捕获错误和处理错误

import React from 'react';

// 错误边界类组件
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false }; // 用于跟踪是否有错误
  }

  // 捕获错误并更新状态
  static getDerivedStateFromError(error) {
    console.log('Error caught:', error);
    return { hasError: true }; // 更新状态以渲染后备 UI
  }

  // 组件渲染出错时调用
  componentDidCatch(error, info) {
    console.log('Error information:', info);
    // 可以在这里执行错误日志记录
  }

  render() {
    if (this.state.hasError) {
      return (
        <h1>Something went wrong. Please try again later.</h1>
      ); // 这里可以返回自定义错误信息或后备 UI
    }

    return this.props.children; // 正常渲染子组件
  }
}

// 一个破坏性的子组件示例
class BuggyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      throwError: false,
    };
  }

  // 触发错误
  handleError = () => {
    this.setState({ throwError: true });
  };

  render() {
    if (this.state.throwError) {
      throw new Error('I crashed!'); // 人为抛出错误
    }
    return (
      <div>
        <h2>Buggy Component</h2>
        <button onClick={this.handleError}>Trigger Error</button>
      </div>
    );
  }
}

// 主应用组件
class App extends React.Component {
  render() {
    return (
      <ErrorBoundary>
        <BuggyComponent />
      </ErrorBoundary>
    );
  }
}

export default App;