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

React componentDidUpdate 的性能优化技巧

2022-06-082.0k 阅读

React componentDidUpdate 的性能优化技巧

在 React 应用开发中,componentDidUpdate 是一个至关重要的生命周期方法。它在组件更新后被调用,这意味着每当组件的 props 或 state 发生变化,且导致重新渲染后,componentDidUpdate 就会执行。然而,如果使用不当,它可能会引发性能问题,导致应用响应迟缓,用户体验变差。因此,掌握 componentDidUpdate 的性能优化技巧显得尤为重要。

理解 componentDidUpdate 的触发机制

componentDidUpdate 在组件更新后立即调用,它接收三个参数:prevProps(更新前的 props)、prevState(更新前的 state)和 snapshot(由 getSnapshotBeforeUpdate 返回的快照值,若未使用该方法则为 undefined)。

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }

  componentDidUpdate(prevProps, prevState, snapshot) {
    console.log('Component updated. Prev props:', prevProps, 'Prev state:', prevState);
  }

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

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

在上述代码中,每次点击按钮,state 中的 count 值变化,组件重新渲染,componentDidUpdate 方法就会被触发,在控制台打印出更新前的 propsstate

避免不必要的更新

  1. 使用 shouldComponentUpdate 进行条件渲染控制 shouldComponentUpdate 是 React 提供的另一个生命周期方法,它允许你通过返回 truefalse 来决定组件是否应该更新。在 shouldComponentUpdate 中进行逻辑判断,可以阻止不必要的 componentDidUpdate 调用。
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          count: 0
        };
      }
    
      shouldComponentUpdate(nextProps, nextState) {
        // 只有当 count 变化时才更新
        return nextState.count!== this.state.count;
      }
    
      componentDidUpdate(prevProps, prevState, snapshot) {
        console.log('Component updated. Prev props:', prevProps, 'Prev state:', prevState);
      }
    
      handleClick = () => {
        this.setState(prevState => ({
          count: prevState.count + 1
        }));
      }
    
      render() {
        return (
          <div>
            <p>Count: {this.state.count}</p>
            <button onClick={this.handleClick}>Increment</button>
          </div>
        );
      }
    }
    
    在这个例子中,shouldComponentUpdate 只在 count 状态变化时返回 true,允许组件更新,否则返回 false,阻止更新,也就不会触发 componentDidUpdate,从而避免了不必要的性能开销。
  2. 使用 React.memo 或 PureComponent
    • React.memo 用于函数组件:React.memo 是 React 16.6 引入的高阶组件,它可以对函数组件进行浅比较。如果组件的 props 没有变化,React.memo 会阻止组件重新渲染,也就不会触发 componentDidUpdate(因为没有更新发生)。
    const MyFunctionComponent = React.memo((props) => {
      return (
        <div>
          <p>{props.value}</p>
        </div>
      );
    });
    
    • PureComponent 用于类组件PureComponentReact.memo 类似,它对 propsstate 进行浅比较。如果 propsstate 没有变化,组件不会重新渲染。
    class MyPureComponent extends React.PureComponent {
      render() {
        return (
          <div>
            <p>{this.props.value}</p>
          </div>
        );
      }
    }
    
    浅比较意味着它只会比较对象或数组的引用,而不会深入比较对象或数组内部的值。如果你的数据结构较为复杂,可能需要自定义比较逻辑来确保正确的性能优化。

优化 componentDidUpdate 中的操作

  1. 避免频繁的 DOM 操作componentDidUpdate 中进行 DOM 操作时,要确保操作是必要的,并且尽量减少操作次数。例如,假设你有一个组件用于显示图片列表,并且在图片加载完成后需要设置图片的尺寸。
    class ImageList extends React.Component {
      constructor(props) {
        super(props);
        this.imageRefs = [];
      }
    
      componentDidUpdate(prevProps) {
        if (this.props.images.length!== prevProps.images.length) {
          this.imageRefs.forEach((ref, index) => {
            if (ref.current) {
              const img = new Image();
              img.src = this.props.images[index].src;
              img.onload = () => {
                ref.current.style.width = img.width + 'px';
                ref.current.style.height = img.height + 'px';
              };
            }
          });
        }
      }
    
      render() {
        return (
          <div>
            {this.props.images.map((image, index) => (
              <img
                key={index}
                ref={(ref) => this.imageRefs[index] = ref}
                src={image.src}
                alt={image.alt}
              />
            ))}
          </div>
        );
      }
    }
    
    在上述代码中,只有当图片列表的长度发生变化时,才会在 componentDidUpdate 中为新添加的图片设置尺寸。这样避免了每次组件更新都进行不必要的 DOM 操作。
  2. 节流和防抖
    • 节流:节流是指在一定时间内,只允许执行一次函数。例如,当你在 componentDidUpdate 中执行一个可能频繁触发的操作,如网络请求,你可以使用节流来限制请求频率。
    function throttle(func, delay) {
      let timer = null;
      return function() {
        if (!timer) {
          func.apply(this, arguments);
          timer = setTimeout(() => {
            timer = null;
          }, delay);
        }
      };
    }
    
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          data: []
        };
        this.fetchData = throttle(this.fetchData, 1000);
      }
    
      componentDidUpdate(prevProps) {
        if (this.props.filter!== prevProps.filter) {
          this.fetchData();
        }
      }
    
      fetchData = () => {
        // 模拟网络请求
        console.log('Fetching data with new filter:', this.props.filter);
      }
    
      render() {
        return (
          <div>
            {/* 组件渲染内容 */}
          </div>
        );
      }
    }
    
    在这个例子中,当 props 中的 filter 变化时,fetchData 方法会被调用,但由于使用了节流,它在 1 秒内只会被调用一次,避免了频繁的网络请求。
    • 防抖:防抖是指在一定时间内,如果再次触发函数,则重新计算时间,直到一定时间内没有再次触发,才执行函数。比如,在搜索框输入时,你可能希望用户停止输入一段时间后再进行搜索请求,以避免频繁请求。
    function debounce(func, delay) {
      let timer = null;
      return function() {
        clearTimeout(timer);
        timer = setTimeout(() => {
          func.apply(this, arguments);
        }, delay);
      };
    }
    
    class SearchComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          searchTerm: ''
        };
        this.search = debounce(this.search, 500);
      }
    
      componentDidUpdate(prevProps, prevState) {
        if (this.state.searchTerm!== prevState.searchTerm) {
          this.search();
        }
      }
    
      handleChange = (e) => {
        this.setState({
          searchTerm: e.target.value
        });
      }
    
      search = () => {
        // 模拟搜索请求
        console.log('Searching with term:', this.state.searchTerm);
      }
    
      render() {
        return (
          <div>
            <input
              type="text"
              value={this.state.searchTerm}
              onChange={this.handleChange}
              placeholder="Search..."
            />
          </div>
        );
      }
    }
    
    这里,当 searchTerm 状态变化时,search 方法会被防抖处理,用户停止输入 500 毫秒后才会执行搜索操作,减少了不必要的请求。

正确处理异步操作

  1. 使用 Promise 和 async/awaitcomponentDidUpdate 中进行异步操作时,使用 Promiseasync/await 可以使代码更易读,并且便于处理异步操作的结果。例如,假设你在组件更新后需要从服务器获取一些额外的数据。
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          additionalData: null
        };
      }
    
      async componentDidUpdate(prevProps) {
        if (this.props.id!== prevProps.id) {
          try {
            const response = await fetch(`/api/data/${this.props.id}`);
            const data = await response.json();
            this.setState({
              additionalData: data
            });
          } catch (error) {
            console.error('Error fetching data:', error);
          }
        }
      }
    
      render() {
        return (
          <div>
            {this.state.additionalData && (
              <p>{JSON.stringify(this.state.additionalData)}</p>
            )}
          </div>
        );
      }
    }
    
    在这个例子中,componentDidUpdate 使用 async/await 来处理异步的 fetch 请求,使得代码结构清晰,并且可以优雅地处理错误。
  2. 取消未完成的异步操作 当组件在异步操作进行中更新,并且新的更新导致需要重新发起异步操作时,取消之前未完成的操作是很重要的,以避免内存泄漏和不必要的资源浪费。例如,使用 AbortController 来取消 fetch 请求。
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          additionalData: null
        };
        this.abortController = new AbortController();
      }
    
      async componentDidUpdate(prevProps) {
        if (this.props.id!== prevProps.id) {
          this.abortController.abort();
          this.abortController = new AbortController();
          try {
            const response = await fetch(`/api/data/${this.props.id}`, {
              signal: this.abortController.signal
            });
            const data = await response.json();
            this.setState({
              additionalData: data
            });
          } catch (error) {
            if (error.name!== 'AbortError') {
              console.error('Error fetching data:', error);
            }
          }
        }
      }
    
      render() {
        return (
          <div>
            {this.state.additionalData && (
              <p>{JSON.stringify(this.state.additionalData)}</p>
            )}
          </div>
        );
      }
    }
    
    在这个代码中,每次 props 中的 id 变化时,先取消之前的 fetch 请求(通过 abortController.abort()),然后创建新的 AbortController 并将其信号传递给新的 fetch 请求。这样可以确保在组件更新时,未完成的异步操作被正确取消。

分析和监控性能

  1. 使用 React DevTools React DevTools 是一款强大的浏览器扩展工具,它可以帮助你分析组件的渲染性能。在 React DevTools 的 Profiler 标签中,你可以记录组件的渲染时间、更新次数等信息。通过分析这些数据,你可以找出在 componentDidUpdate 中可能存在的性能瓶颈。例如,你可以看到某个组件在每次更新时都花费了较长时间在 componentDidUpdate 中的特定操作上,从而针对性地进行优化。
  2. 使用 console.time 和 console.timeEndcomponentDidUpdate 中使用 console.timeconsole.timeEnd 可以简单地测量某个操作的执行时间。例如:
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          data: []
        };
      }
    
      componentDidUpdate(prevProps) {
        if (this.props.someProp!== prevProps.someProp) {
          console.time('DataProcessing');
          // 复杂的数据处理操作
          const newData = this.props.someProp.map(item => item * 2);
          this.setState({
            data: newData
          });
          console.timeEnd('DataProcessing');
        }
      }
    
      render() {
        return (
          <div>
            {/* 组件渲染内容 */}
          </div>
        );
      }
    }
    
    通过这种方式,你可以在控制台看到 DataProcessing 操作所花费的时间,以便评估其对性能的影响,并进行优化。

处理复杂数据结构的更新

  1. 深度比较propsstate 包含复杂的数据结构,如多层嵌套的对象或数组时,浅比较(如 PureComponentReact.memo 提供的)可能无法满足需求。在这种情况下,你需要进行深度比较来决定组件是否应该更新。例如,使用 lodash 库的 isEqual 方法进行深度比较。
    import _ from 'lodash';
    
    class ComplexComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          complexData: {
            subData: {
              value: 1
            }
          }
        };
      }
    
      shouldComponentUpdate(nextProps, nextState) {
        return!_.isEqual(nextProps.complexProp, this.props.complexProp) ||
         !_.isEqual(nextState.complexData, this.state.complexData);
      }
    
      componentDidUpdate(prevProps, prevState) {
        console.log('Component updated. Prev props:', prevProps, 'Prev state:', prevState);
      }
    
      handleClick = () => {
        this.setState(prevState => ({
          complexData: {
            subData: {
             ...prevState.complexData.subData,
              value: prevState.complexData.subData.value + 1
            }
          }
        }));
      }
    
      render() {
        return (
          <div>
            <p>{JSON.stringify(this.state.complexData)}</p>
            <button onClick={this.handleClick}>Update Complex Data</button>
          </div>
        );
      }
    }
    
    在这个例子中,shouldComponentUpdate 使用 _.isEqual 对复杂的 propsstate 数据进行深度比较,只有当数据真正发生变化时才允许组件更新,进而触发 componentDidUpdate
  2. 不可变数据更新模式 始终使用不可变数据更新模式可以避免一些潜在的性能问题。例如,在更新对象时,使用 ... 扩展运算符创建新的对象,而不是直接修改原对象。
    class MyComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          user: {
            name: 'John',
            age: 30
          }
        };
      }
    
      handleUpdate = () => {
        this.setState(prevState => ({
          user: {
            ...prevState.user,
            age: prevState.user.age + 1
          }
        }));
      }
    
      componentDidUpdate(prevProps, prevState) {
        if (!_.isEqual(prevState.user, this.state.user)) {
          console.log('User data updated');
        }
      }
    
      render() {
        return (
          <div>
            <p>{JSON.stringify(this.state.user)}</p>
            <button onClick={this.handleUpdate}>Update User</button>
          </div>
        );
      }
    }
    
    通过这种方式,React 可以更有效地检测到数据变化,并且有助于在 componentDidUpdate 中进行更准确的逻辑处理。

优化跨组件通信导致的更新

  1. 减少不必要的 prop 传递 在 React 应用中,跨组件通信通常通过 props 传递来实现。然而,如果传递的 props 过多或不必要,可能会导致不必要的组件更新。例如,有一个父组件 ParentComponent 包含子组件 ChildComponentParentComponent 有一些状态数据,但其中部分数据实际上与 ChildComponent 无关。
    class ParentComponent extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          data1: 'Some data 1',
          data2: 'Some data 2',
          data3: 'Some data 3'
        };
      }
    
      render() {
        // 只传递 ChildComponent 需要的 props
        return (
          <div>
            <ChildComponent data={this.state.data2} />
          </div>
        );
      }
    }
    
    class ChildComponent extends React.Component {
      componentDidUpdate(prevProps) {
        if (this.props.data!== prevProps.data) {
          console.log('Child component updated due to data change');
        }
      }
    
      render() {
        return (
          <div>
            <p>{this.props.data}</p>
          </div>
        );
      }
    }
    
    在这个例子中,ParentComponent 只向 ChildComponent 传递了它需要的 data2,这样当 data1data3 变化时,不会触发 ChildComponent 的更新,从而避免了不必要的 componentDidUpdate 调用。
  2. 使用 Context 优化共享数据传递 当多个组件需要共享一些数据时,通过层层传递 props 会变得繁琐且容易导致不必要的更新。这时可以使用 React Context。例如,假设有一个应用有多个组件需要访问用户的登录信息。
    const UserContext = React.createContext();
    
    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          user: {
            name: 'John',
            isLoggedIn: true
          }
        };
      }
    
      render() {
        return (
          <UserContext.Provider value={this.state.user}>
            <ComponentA />
          </UserContext.Provider>
        );
      }
    }
    
    class ComponentA extends React.Component {
      componentDidUpdate(prevProps) {
        // 这里不需要担心来自上层不必要的 prop 传递导致的更新
        console.log('Component A updated');
      }
    
      render() {
        return (
          <div>
            <ComponentB />
          </div>
        );
      }
    }
    
    class ComponentB extends React.Component {
      constructor(props) {
        super(props);
        this.user = React.useContext(UserContext);
      }
    
      componentDidUpdate(prevProps) {
        // 当 UserContext 中的数据变化时才更新
        console.log('Component B updated due to user context change');
      }
    
      render() {
        return (
          <div>
            <p>{this.user.name}</p>
          </div>
        );
      }
    }
    
    在这个例子中,UserContext 提供了用户信息,ComponentB 可以直接从 Context 中获取数据,而不需要通过层层传递 props。这样,当 App 组件中的其他与用户信息无关的状态变化时,不会触发 ComponentB 的不必要更新,优化了 componentDidUpdate 的调用。

通过上述一系列性能优化技巧,可以有效地提升 React 应用在 componentDidUpdate 生命周期方法中的性能表现,使应用更加流畅和高效,为用户提供更好的体验。在实际开发中,需要根据具体的应用场景和需求,综合运用这些技巧来实现最佳的性能优化效果。