MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 生命周期与性能调优的关系

2021-12-236.5k 阅读

React 生命周期概述

在 React 中,组件的生命周期是指从组件被创建到被销毁的整个过程。React 提供了一系列的生命周期方法,让开发者可以在组件生命周期的不同阶段执行特定的操作。这些方法可以分为三个主要阶段:挂载阶段(Mounting)、更新阶段(Updating)和卸载阶段(Unmounting)。

挂载阶段

  1. constructor:这是 ES6 类的构造函数,在组件创建时最先被调用。通常用于初始化 state 和绑定事件处理函数。例如:
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState({
      count: this.state.count + 1
    });
  }
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Increment</button>
      </div>
    );
  }
}

在上述代码中,constructor 方法初始化了 state 中的 count 变量,并绑定了 handleClick 方法。

  1. static getDerivedStateFromProps:这是一个静态方法,在组件挂载和更新时都会被调用。它的作用是根据 props 的变化来更新 state。例如:
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: props.initialValue
    };
  }
  static getDerivedStateFromProps(props, state) {
    if (props.initialValue!== state.value) {
      return {
        value: props.initialValue
      };
    }
    return null;
  }
  render() {
    return <div>{this.state.value}</div>;
  }
}

在这个例子中,当 props 中的 initialValue 发生变化时,getDerivedStateFromProps 方法会更新 state 中的 value

  1. render:这是组件中唯一必须实现的方法。它负责返回 React 元素,描述组件的 UI 结构。例如:
class MyComponent extends React.Component {
  render() {
    return <div>Hello, React!</div>;
  }
}

render 方法应该是纯函数,不应该引起副作用。

  1. componentDidMount:在组件被插入到 DOM 后调用。通常用于执行需要 DOM 节点的操作,如初始化第三方库、订阅事件等。例如:
class MyComponent extends React.Component {
  componentDidMount() {
    document.title = 'My React Component';
  }
  render() {
    return <div>Component has been mounted.</div>;
  }
}

在这个例子中,componentDidMount 方法修改了页面的标题。

更新阶段

  1. shouldComponentUpdate:在组件接收到新的 propsstate 时被调用,用于决定组件是否需要更新。返回 true 表示需要更新,返回 false 表示不需要更新。这是一个重要的性能优化点。例如:
class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.value!== nextProps.value) {
      return true;
    }
    return false;
  }
  render() {
    return <div>{this.props.value}</div>;
  }
}

在这个例子中,只有当 props 中的 value 发生变化时,组件才会更新。

  1. static getDerivedStateFromProps:同挂载阶段,在更新时也会被调用,用于根据新的 props 更新 state

  2. render:同挂载阶段,重新渲染组件以反映新的 propsstate

  3. getSnapshotBeforeUpdate:在 render 之后,componentDidUpdate 之前调用。它可以捕获一些在更新前的 DOM 状态,例如滚动位置。例如:

class MyList extends React.Component {
  constructor(props) {
    super(props);
    this.listRef = React.createRef();
  }
  getSnapshotBeforeUpdate(prevProps, prevState) {
    if (prevProps.items.length < this.props.items.length) {
      return this.listRef.current.scrollHeight - prevState.scrollHeight;
    }
    return null;
  }
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot) {
      this.listRef.current.scrollTop += snapshot;
    }
  }
  render() {
    return (
      <div ref={this.listRef}>
        {this.props.items.map((item, index) => (
          <div key={index}>{item}</div>
        ))}
      </div>
    );
  }
}

在这个例子中,getSnapshotBeforeUpdate 捕获了列表增加新项前的滚动高度变化,componentDidUpdate 根据这个变化调整滚动位置。

  1. componentDidUpdate:在组件更新后被调用。可以用于执行依赖于 DOM 更新的操作,如操作新的 DOM 节点、发送网络请求等。例如:
class MyComponent extends React.Component {
  componentDidUpdate(prevProps, prevState) {
    if (prevProps.value!== this.props.value) {
      console.log('Value has changed:', this.props.value);
    }
  }
  render() {
    return <div>{this.props.value}</div>;
  }
}

在这个例子中,componentDidUpdate 方法在 props 中的 value 变化时打印日志。

卸载阶段

componentWillUnmount:在组件从 DOM 中移除前被调用。通常用于清理副作用,如取消定时器、取消网络请求、解绑事件等。例如:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.timer = null;
  }
  componentDidMount() {
    this.timer = setInterval(() => {
      console.log('Timer is running');
    }, 1000);
  }
  componentWillUnmount() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }
  render() {
    return <div>Component is mounted.</div>;
  }
}

在这个例子中,componentWillUnmount 方法清除了在 componentDidMount 中设置的定时器。

React 生命周期与性能调优的紧密联系

利用 shouldComponentUpdate 避免不必要的渲染

shouldComponentUpdate 方法在性能调优中起着关键作用。React 应用中,频繁的渲染会导致性能下降,尤其是在组件树较大的情况下。通过合理实现 shouldComponentUpdate,可以阻止不必要的渲染,从而提升性能。

假设我们有一个展示用户信息的组件,用户信息包含姓名和年龄:

class UserInfo extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.user.name!== nextProps.user.name) {
      return true;
    }
    return false;
  }
  render() {
    return (
      <div>
        <p>Name: {this.props.user.name}</p>
        <p>Age: {this.props.user.age}</p>
      </div>
    );
  }
}

在这个例子中,只有当 propsusername 发生变化时,组件才会更新。如果只是 age 变化,由于 shouldComponentUpdate 返回 false,组件不会重新渲染,节省了计算资源。

然而,在实际应用中,比较 propsstate 的值可能会更复杂。对于复杂的对象,直接比较引用可能会导致误判。例如:

class ComplexComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.data!== nextProps.data;
  }
  render() {
    return <div>{JSON.stringify(this.props.data)}</div>;
  }
}

这里如果 props.data 是一个对象,即使对象内部的属性发生了变化,但对象的引用没有改变,shouldComponentUpdate 会返回 false,导致组件不会更新。为了解决这个问题,可以使用深度比较的方法,比如使用 lodash 库的 isEqual 方法:

import _ from 'lodash';
class ComplexComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    return!_.isEqual(this.props.data, nextProps.data);
  }
  render() {
    return <div>{JSON.stringify(this.props.data)}</div>;
  }
}

通过深度比较,可以确保在对象内部属性变化时,组件能够正确地更新。

在 componentDidMount 和 componentWillUnmount 中管理副作用

componentDidMount 中执行的操作,如初始化第三方库、订阅事件等,如果不进行正确的清理,可能会导致内存泄漏和性能问题。例如,在一个图表组件中使用 chart.js 库:

import React from'react';
import Chart from 'chart.js';

class ChartComponent extends React.Component {
  constructor(props) {
    super(props);
    this.chartRef = React.createRef();
  }
  componentDidMount() {
    this.chart = new Chart(this.chartRef.current, {
      type: 'bar',
      data: {
        labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'],
        datasets: [
          {
            label: '# of Votes',
            data: [12, 19, 3, 5, 2, 3],
            backgroundColor: [
              'rgba(255, 99, 132, 0.2)',
              'rgba(54, 162, 235, 0.2)',
              'rgba(255, 206, 86, 0.2)',
              'rgba(75, 192, 192, 0.2)',
              'rgba(153, 102, 255, 0.2)',
              'rgba(255, 159, 64, 0.2)'
            ],
            borderColor: [
              'rgba(255, 99, 132, 1)',
              'rgba(54, 162, 235, 1)',
              'rgba(255, 206, 86, 1)',
              'rgba(75, 192, 192, 1)',
              'rgba(153, 102, 255, 1)',
              'rgba(255, 159, 64, 1)'
            ],
            borderWidth: 1
          }
        ]
      },
      options: {
        scales: {
          yAxes: [
            {
              ticks: {
                beginAtZero: true
              }
            }
          ]
        }
      }
    });
  }
  componentWillUnmount() {
    if (this.chart) {
      this.chart.destroy();
    }
  }
  render() {
    return <canvas ref={this.chartRef}></canvas>;
  }
}

componentDidMount 中初始化了图表,而在 componentWillUnmount 中销毁图表,这样可以避免内存泄漏。如果没有 componentWillUnmount 中的清理操作,当组件被卸载时,图表实例仍然存在,可能会占用内存,并且如果再次挂载相同的组件,可能会出现重复渲染或冲突的问题。

利用 getSnapshotBeforeUpdate 和 componentDidUpdate 处理 DOM 相关的性能优化

getSnapshotBeforeUpdatecomponentDidUpdate 这两个生命周期方法对于处理 DOM 相关的性能优化非常有用。例如,在一个聊天窗口组件中,当有新消息到来时,我们希望聊天窗口自动滚动到最新消息:

class ChatWindow extends React.Component {
  constructor(props) {
    super(props);
    this.chatRef = React.createRef();
  }
  getSnapshotBeforeUpdate(prevProps, prevState) {
    if (prevProps.messages.length < this.props.messages.length) {
      const chatContainer = this.chatRef.current;
      return chatContainer.scrollHeight - chatContainer.scrollTop;
    }
    return null;
  }
  componentDidUpdate(prevProps, prevState, snapshot) {
    if (snapshot) {
      const chatContainer = this.chatRef.current;
      chatContainer.scrollTop = chatContainer.scrollHeight - snapshot;
    }
  }
  render() {
    return (
      <div ref={this.chatRef}>
        {this.props.messages.map((message, index) => (
          <div key={index}>{message}</div>
        ))}
      </div>
    );
  }
}

在这个例子中,getSnapshotBeforeUpdate 捕获了新消息到来前聊天窗口的滚动位置信息,componentDidUpdate 根据这个信息将聊天窗口滚动到最新消息的位置。这样可以保证用户在新消息到来时,聊天窗口能够自动显示最新内容,并且通过精确的滚动计算,避免了不必要的滚动操作,提升了用户体验和性能。

避免在 render 方法中产生副作用

render 方法应该是纯函数,即给定相同的 propsstate,应该始终返回相同的结果,并且不应该引起副作用。例如,不要在 render 方法中进行网络请求或修改 DOM:

// 错误示例
class MyComponent extends React.Component {
  render() {
    // 不应该在 render 中进行网络请求
    fetch('https://example.com/api/data')
    .then(response => response.json())
    .then(data => console.log(data));
    return <div>Component</div>;
  }
}

这样做会导致每次组件渲染时都进行网络请求,不仅浪费资源,还可能导致数据不一致。正确的做法是将网络请求放在 componentDidMountcomponentDidUpdate 中:

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      data: null
    };
  }
  componentDidMount() {
    fetch('https://example.com/api/data')
    .then(response => response.json())
    .then(data => this.setState({ data }));
  }
  render() {
    return (
      <div>
        {this.state.data? <p>{JSON.stringify(this.state.data)}</p> : <p>Loading...</p>}
      </div>
    );
  }
}

通过将网络请求放在 componentDidMount 中,确保只在组件挂载时进行一次请求,提高了性能并保证了数据的一致性。

实际应用中的性能调优策略结合生命周期

列表渲染优化

在 React 应用中,列表渲染是常见的场景。如果列表项较多,性能问题可能会很明显。结合 React 生命周期,可以采用以下优化策略。

假设有一个展示商品列表的组件:

class ProductList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      products: []
    };
  }
  componentDidMount() {
    // 模拟从 API 获取商品数据
    setTimeout(() => {
      const products = [
        { id: 1, name: 'Product 1', price: 100 },
        { id: 2, name: 'Product 2', price: 200 },
        { id: 3, name: 'Product 3', price: 300 }
      ];
      this.setState({ products });
    }, 1000);
  }
  shouldComponentUpdate(nextProps, nextState) {
    return this.state.products!== nextState.products;
  }
  render() {
    return (
      <ul>
        {this.state.products.map(product => (
          <li key={product.id}>
            {product.name} - ${product.price}
          </li>
        ))}
      </ul>
    );
  }
}

在这个例子中,shouldComponentUpdate 方法简单地比较了 products 数组的引用。如果 products 数组内部结构发生变化但引用不变,可能会导致组件不更新。为了更精确地控制更新,可以使用 React.memo 对列表项组件进行包裹。

首先,创建一个单独的 ProductItem 组件:

const ProductItem = React.memo(({ product }) => (
  <li>
    {product.name} - ${product.price}
  </li>
));

然后,在 ProductList 组件中使用 ProductItem

class ProductList extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      products: []
    };
  }
  componentDidMount() {
    // 模拟从 API 获取商品数据
    setTimeout(() => {
      const products = [
        { id: 1, name: 'Product 1', price: 100 },
        { id: 2, name: 'Product 2', price: 200 },
        { id: 3, name: 'Product 3', price: 300 }
      ];
      this.setState({ products });
    }, 1000);
  }
  render() {
    return (
      <ul>
        {this.state.products.map(product => (
          <ProductItem key={product.id} product={product} />
        ))}
      </ul>
    );
  }
}

React.memo 会对 ProductItem 组件的 props 进行浅比较,如果 props 没有变化,组件不会重新渲染。这样可以有效减少列表项的不必要渲染,提升性能。

条件渲染与性能

在 React 中,条件渲染是根据条件决定是否渲染某个组件或元素。合理的条件渲染可以避免不必要的组件渲染,从而提高性能。

例如,有一个根据用户登录状态显示不同内容的组件:

class UserContent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isLoggedIn: false
    };
  }
  handleLogin = () => {
    this.setState({ isLoggedIn: true });
  };
  handleLogout = () => {
    this.setState({ isLoggedIn: false });
  };
  render() {
    return (
      <div>
        {this.state.isLoggedIn? (
          <div>
            <p>Welcome, user!</p>
            <button onClick={this.handleLogout}>Logout</button>
          </div>
        ) : (
          <button onClick={this.handleLogin}>Login</button>
        )}
      </div>
    );
  }
}

在这个例子中,根据 isLoggedIn 的状态,只渲染登录或注销相关的内容,避免了不必要的组件渲染。如果不使用条件渲染,可能会导致登录和注销的元素都被渲染,即使它们在当前状态下不应该显示,从而浪费性能。

代码分割与懒加载提升性能

代码分割和懒加载是现代 React 应用中重要的性能优化手段,并且可以与 React 生命周期相结合。

假设我们有一个大型应用,其中有一个比较复杂的图表组件,只有在用户点击特定按钮时才需要加载。我们可以使用 React.lazy 和 Suspense 来实现懒加载:

const ChartComponent = React.lazy(() => import('./ChartComponent'));

class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      showChart: false
    };
  }
  handleClick = () => {
    this.setState({ showChart:!this.state.showChart });
  };
  render() {
    return (
      <div>
        <button onClick={this.handleClick}>
          {this.state.showChart? 'Hide Chart' : 'Show Chart'}
        </button>
        {this.state.showChart && (
          <React.Suspense fallback={<div>Loading chart...</div>}>
            <ChartComponent />
          </React.Suspense>
        )}
      </div>
    );
  }
}

在这个例子中,ChartComponent 只有在用户点击按钮显示图表时才会加载。React.lazy 用于动态导入组件,Suspense 组件在组件加载时显示加载提示。这样可以避免在应用启动时加载不必要的代码,提升应用的初始加载性能。同时,结合 shouldComponentUpdate 等生命周期方法,可以进一步控制图表组件的更新,避免不必要的渲染。

性能监测与优化实践

使用 React DevTools 进行性能分析

React DevTools 是 React 官方提供的浏览器扩展,它可以帮助开发者分析组件的性能。在 Chrome 或 Firefox 浏览器中安装 React DevTools 后,可以在开发者工具中看到 React 相关的面板。

在 React DevTools 的 Profiler 选项卡中,可以录制组件的渲染过程,分析每个组件的渲染时间、更新次数等信息。例如,在一个复杂的表单应用中,通过录制渲染过程,可以发现某个表单输入组件的渲染时间过长。

class FormInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: ''
    };
  }
  handleChange = (e) => {
    this.setState({ value: e.target.value });
  };
  render() {
    return (
      <input
        type="text"
        value={this.state.value}
        onChange={this.handleChange}
        placeholder={this.props.placeholder}
      />
    );
  }
}

通过 React DevTools 的 Profiler 分析,可能会发现每次输入框的值变化时,组件都会重新渲染,并且渲染时间较长。进一步检查发现,shouldComponentUpdate 方法没有正确实现,导致不必要的渲染。通过优化 shouldComponentUpdate 方法,如只在 props 中的 placeholderstate 中的 value 变化时才更新:

class FormInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: ''
    };
  }
  handleChange = (e) => {
    this.setState({ value: e.target.value });
  };
  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.placeholder!== nextProps.placeholder || this.state.value!== nextState.value) {
      return true;
    }
    return false;
  }
  render() {
    return (
      <input
        type="text"
        value={this.state.value}
        onChange={this.handleChange}
        placeholder={this.props.placeholder}
      />
    );
  }
}

再次使用 React DevTools 的 Profiler 进行分析,可以看到组件的渲染次数和渲染时间都有明显减少,性能得到提升。

优化工具与库的使用

除了 React DevTools,还有一些其他的工具和库可以帮助进行性能优化。例如,lodash 库提供了许多实用的函数,如 debouncethrottle,可以用于控制函数的调用频率,避免在短时间内频繁触发导致性能问题。

假设我们有一个搜索框组件,当用户输入时会触发搜索请求:

import React from'react';
import _ from 'lodash';

class SearchInput extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      searchText: ''
    };
    this.debouncedSearch = _.debounce(this.search, 300);
  }
  handleChange = (e) => {
    this.setState({ searchText: e.target.value });
    this.debouncedSearch(e.target.value);
  }
  search = (text) => {
    // 模拟搜索请求
    console.log('Searching for:', text);
  }
  componentWillUnmount() {
    this.debouncedSearch.cancel();
  }
  render() {
    return (
      <input
        type="text"
        value={this.state.searchText}
        onChange={this.handleChange}
        placeholder="Search..."
      />
    );
  }
}

在这个例子中,_.debounce 函数将 search 函数进行防抖处理,只有在用户停止输入 300 毫秒后才会触发搜索请求,避免了用户在快速输入时频繁触发请求导致的性能问题。同时,在 componentWillUnmount 中取消 debounce,防止内存泄漏。

另外,react - pure - render - mixin 库可以帮助简化 shouldComponentUpdate 的实现。虽然 React 已经提供了 React.memoshouldComponentUpdate 方法,但这个库提供了一种更简洁的方式来进行浅比较。例如:

import React from'react';
import PureRenderMixin from'react - pure - render - mixin';

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.shouldComponentUpdate = PureRenderMixin.shouldComponentUpdate.bind(this);
  }
  render() {
    return <div>{this.props.value}</div>;
  }
}

通过引入这个库,shouldComponentUpdate 方法会自动对 propsstate 进行浅比较,减少了手动编写比较逻辑的工作量,并且有助于提高性能。

总结 React 生命周期在性能调优中的关键作用

React 生命周期方法为开发者提供了在组件不同阶段进行性能优化的机会。从挂载阶段的合理初始化和副作用管理,到更新阶段通过 shouldComponentUpdate 避免不必要的渲染,再到卸载阶段的资源清理,每个生命周期阶段都与性能紧密相关。

在实际应用中,结合性能监测工具如 React DevTools,以及各种优化工具和库,能够更有效地利用 React 生命周期进行性能调优。无论是简单的组件还是复杂的应用,通过深入理解和合理运用 React 生命周期与性能调优的关系,都可以提升应用的性能,提供更好的用户体验。同时,随着 React 框架的不断发展,新的特性和优化方式也会不断出现,开发者需要持续关注和学习,以保持应用的高性能。