MST

星途 面试题库

面试题:React函数组件使用useState和类组件使用this.setState的性能差异

从React的渲染机制和优化策略角度出发,分析函数组件中useState和类组件中this.setState在性能表现上的差异,以及在不同场景下如何选择以达到更好的性能。
16.1万 热度难度
前端开发React

知识考点

AI 面试

面试题答案

一键面试

渲染机制差异

  1. useState
    • 在函数组件中,useState 每次更新状态都会触发函数组件的重新渲染。React 会对比前后两次渲染的 JSX 结构,进行虚拟 DOM 差异比较(diffing 算法),从而决定实际 DOM 要更新的部分。由于函数组件每次渲染都是全新的函数调用,函数内部的变量和状态是离散的,每次渲染都有独立的作用域。例如:
    import React, { useState } from'react';
    const MyFunctionComponent = () => {
        const [count, setCount] = useState(0);
        return (
            <div>
                <p>Count: {count}</p>
                <button onClick={() => setCount(count + 1)}>Increment</button>
            </div>
        );
    };
    
    • 每次调用 setCount 都会重新运行 MyFunctionComponent 函数,重新创建函数内部的变量和 JSX 结构。
  2. this.setState
    • 在类组件中,this.setState 触发的是类组件的 render 方法重新执行。类组件的状态和方法都在类的实例上,状态更新后,React 同样会进行虚拟 DOM 的 diffing 算法来确定实际 DOM 的更新。但是,类组件的 render 方法是在类的原型上,状态和方法共享同一个实例。例如:
    import React, { Component } from'react';
    class MyClassComponent extends Component {
        constructor(props) {
            super(props);
            this.state = {
                count: 0
            };
        }
        increment = () => {
            this.setState({
                count: this.state.count + 1
            });
        };
        render() {
            return (
                <div>
                    <p>Count: {this.state.count}</p>
                    <button onClick={this.increment}>Increment</button>
                </div>
            );
        }
    }
    
    • 调用 this.setState 时,render 方法重新执行,但类实例上的其他方法和状态依然存在,不会像函数组件那样重新创建整个作用域。

性能表现差异

  1. useState
    • 函数组件的重新渲染粒度相对较细,每次更新状态都会重新执行整个函数组件。如果函数组件内部有复杂的计算或者副作用操作(如订阅事件、定时器等),可能会导致性能问题,因为这些操作在每次渲染时都会重新执行。例如,如果函数组件中有一个复杂的计算函数 expensiveCalculation,每次状态更新都会重新计算:
    import React, { useState } from'react';
    const expensiveCalculation = () => {
        // 复杂计算逻辑
        let result = 0;
        for (let i = 0; i < 1000000; i++) {
            result += i;
        }
        return result;
    };
    const MyFunctionComponent = () => {
        const [count, setCount] = useState(0);
        const calculationResult = expensiveCalculation();
        return (
            <div>
                <p>Count: {count}</p>
                <p>Calculation Result: {calculationResult}</p>
                <button onClick={() => setCount(count + 1)}>Increment</button>
            </div>
        );
    };
    
    • 不过,通过 useMemouseCallback 等钩子可以对函数组件的性能进行优化,缓存计算结果和回调函数,避免不必要的重新计算和创建。
  2. this.setState
    • 类组件的 render 方法重新执行时,虽然也是重新渲染,但由于类实例的状态和方法共享,不会像函数组件那样重新创建作用域和变量。然而,如果类组件的 render 方法中有复杂的计算逻辑,同样会在每次 setState 触发 render 时重新计算。例如:
    import React, { Component } from'react';
    class MyClassComponent extends Component {
        constructor(props) {
            super(props);
            this.state = {
                count: 0
            };
        }
        expensiveCalculation = () => {
            // 复杂计算逻辑
            let result = 0;
            for (let i = 0; i < 1000000; i++) {
                result += i;
            }
            return result;
        };
        increment = () => {
            this.setState({
                count: this.state.count + 1
            });
        };
        render() {
            const calculationResult = this.expensiveCalculation();
            return (
                <div>
                    <p>Count: {this.state.count}</p>
                    <p>Calculation Result: {calculationResult}</p>
                    <button onClick={this.increment}>Increment</button>
                </div>
            );
        }
    }
    
    • 类组件可以通过 shouldComponentUpdate 生命周期方法来手动控制是否重新渲染,避免不必要的 render 调用,而函数组件没有类似的内置方法(虽然可以通过 React.memo 达到类似效果,但原理和使用场景略有不同)。

不同场景下的选择

  1. 简单展示组件
    • useState:对于简单的展示组件,如只显示一个数字或文本并提供简单的交互(如点击按钮更新数字),函数组件配合 useState 非常合适。代码简洁,开发效率高,而且由于组件简单,重新渲染带来的性能开销可以忽略不计。例如上面的简单计数器组件。
    • this.setState:同样可以实现简单展示组件的功能,但类组件的语法相对复杂,需要定义类、构造函数、方法等,对于这种简单场景,使用类组件显得过于繁琐,从开发效率和代码简洁性角度,useState 更优。
  2. 复杂逻辑组件
    • useState:如果组件有复杂的逻辑,如多个状态管理、副作用操作等,通过合理使用 useEffectuseMemouseCallback 等钩子可以有效地管理副作用和优化性能。例如,一个需要根据多个状态变化进行复杂计算并订阅外部数据源的组件,可以通过 useMemo 缓存计算结果,useEffect 处理订阅和取消订阅逻辑。
    • this.setState:类组件在复杂逻辑场景下可以利用 shouldComponentUpdate 生命周期方法进行精细的渲染控制。如果能够准确判断哪些状态变化会影响组件的渲染,通过 shouldComponentUpdate 返回 false 可以避免不必要的 render 调用,从而提升性能。例如,一个具有多个表单输入和复杂业务逻辑的组件,可以在 shouldComponentUpdate 中判断哪些输入值的变化需要重新渲染组件。
  3. 性能敏感场景
    • useState:在性能敏感场景下,需要充分利用 useMemouseCallback 来优化函数组件的性能。例如,如果组件依赖于一个频繁变化但计算昂贵的数据,使用 useMemo 缓存计算结果可以避免每次渲染都重新计算。
    • this.setState:对于性能敏感场景,类组件可以通过 shouldComponentUpdate 进行更灵活的渲染控制。如果对组件渲染的性能要求极高,并且能够准确把握状态变化对渲染的影响,类组件的 shouldComponentUpdate 可能会比函数组件的 React.memo 提供更细粒度的控制。但需要注意的是,shouldComponentUpdate 的实现需要谨慎,否则可能会导致错误的渲染优化。

总的来说,在大多数现代 React 开发中,函数组件配合 useState 因其简洁性和灵活性受到广泛使用。但在一些对渲染控制有特殊要求或者复杂业务逻辑场景下,类组件及其 this.setState 依然有其优势,开发者需要根据具体场景选择合适的方式来达到更好的性能。