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

React函数组件与类组件的区别

2024-12-154.2k 阅读

函数组件与类组件的基本概念

函数组件

在 React 中,函数组件是最基础的组件形式。它本质上就是 JavaScript 函数,接收一个 props 对象作为参数,并返回一个 React 元素。这些函数通常是无状态且无实例的,简单直接地将输入的 props 映射到输出的 UI。以下是一个简单的函数组件示例:

import React from'react';

const HelloWorld = (props) => {
    return <div>Hello, {props.name}</div>;
};

export default HelloWorld;

在上述代码中,HelloWorld 函数接收 props 参数,解构出 name 属性,并将其嵌入到返回的 div 元素中。这是一个典型的函数组件,简洁明了,只负责展示数据。

类组件

类组件则是基于 ES6 类的概念来定义的。它们继承自 React.Component,具有自己的状态(state)和生命周期方法。类组件可以管理自身的状态变化,并在状态或 props 改变时重新渲染。以下是一个简单的类组件示例:

import React, { Component } from'react';

class HelloWorldClass extends Component {
    constructor(props) {
        super(props);
        this.state = {
            message: 'Initial message'
        };
    }

    render() {
        return <div>{this.state.message}</div>;
    }
}

export default HelloWorldClass;

在这个类组件中,通过 constructor 方法初始化了组件的 state,并在 render 方法中展示 state 中的数据。与函数组件相比,类组件结构更为复杂,但提供了更多的功能,如状态管理和生命周期控制。

状态管理的区别

函数组件的状态管理

在 React Hook 出现之前,函数组件没有内置的状态管理机制,因为它们被设计为无状态组件。但随着 React Hook 的引入,函数组件也能轻松管理状态。useState 是 React 提供的一个 Hook,用于在函数组件中添加状态。以下是使用 useState 的示例:

import React, { useState } from'react';

const Counter = () => {
    const [count, setCount] = useState(0);

    const increment = () => {
        setCount(count + 1);
    };

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={increment}>Increment</button>
        </div>
    );
};

export default Counter;

在上述代码中,useState 接受一个初始值 0,并返回一个数组,第一个元素 count 是当前状态值,第二个元素 setCount 是用于更新状态的函数。每次点击按钮调用 increment 函数时,通过 setCount 更新 count 的值,从而触发组件重新渲染。

类组件的状态管理

类组件通过 this.state 来管理状态。状态的更新必须通过 setState 方法,不能直接修改 this.state。例如:

import React, { Component } from'react';

class CounterClass extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
        this.increment = this.increment.bind(this);
    }

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

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

export default CounterClass;

在这个类组件中,increment 方法通过 setState 更新 count 的值。setState 可以接受一个对象或一个函数,这里使用函数形式是为了确保在更新状态时能获取到最新的 prevState,避免在复杂状态更新时出现问题。

状态管理区别的本质

函数组件使用 useState 进行状态管理,更加简洁直观,状态更新逻辑更接近函数式编程风格,每个 useState 独立管理一个状态变量。而类组件通过 this.statesetState 管理状态,这种方式基于面向对象编程的思想,状态与组件实例紧密相关,setState 的批量更新机制使得状态更新更加复杂。例如,在类组件中,如果多次调用 setState,React 会将这些更新合并,在适当的时候一次性更新 DOM,以提高性能。而函数组件中,每次调用 setState 形式的 useState 更新函数,都会触发一次重新渲染,虽然简单直接,但在某些复杂场景下可能需要更多的性能优化。

生命周期方法的区别

函数组件的 “生命周期”

在 React Hook 出现之前,函数组件没有生命周期方法,因为它们没有实例,也就不存在生命周期的概念。但通过使用 useEffect 这个 Hook,函数组件可以模拟类组件的一些生命周期行为。useEffect 可以在组件渲染后执行副作用操作,如数据获取、订阅事件等。它接收两个参数,第一个是一个回调函数,第二个是一个依赖数组。

模拟 componentDidMount

import React, { useEffect } from'react';

const DataComponent = () => {
    useEffect(() => {
        // 模拟 componentDidMount 行为,如数据获取
        fetch('https://example.com/api/data')
          .then(response => response.json())
          .then(data => console.log(data));
        return () => {
            // 模拟 componentWillUnmount 行为,如取消订阅
        };
    }, []);

    return <div>Data component</div>;
};

export default DataComponent;

在上述代码中,当 DataComponent 首次渲染时,useEffect 中的回调函数会被执行,模拟了 componentDidMount 的行为。依赖数组为空,表示这个副作用只在组件挂载时执行一次。

模拟 componentDidUpdate

import React, { useEffect, useState } from'react';

const CounterEffect = () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log(`Count has updated to: ${count}`);
        return () => {
            console.log(`Cleanup when count changes`);
        };
    }, [count]);

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment</button>
        </div>
    );
};

export default CounterEffect;

这里依赖数组中包含 count,表示当 count 状态变化时,useEffect 中的回调函数会被执行,模拟了 componentDidUpdate 的行为。同时,useEffect 回调函数返回的函数会在组件卸载或依赖变化时执行,可用于清理副作用,如取消订阅等操作。

类组件的生命周期

类组件有一套完整的生命周期方法,包括挂载阶段(constructorcomponentWillMountrendercomponentDidMount)、更新阶段(componentWillReceivePropsshouldComponentUpdatecomponentWillUpdaterendercomponentDidUpdate)和卸载阶段(componentWillUnmount)。

挂载阶段

import React, { Component } from'react';

class MountExample extends Component {
    constructor(props) {
        super(props);
        console.log('Constructor called');
    }

    componentWillMount() {
        console.log('componentWillMount called');
    }

    render() {
        console.log('Render called');
        return <div>Mount example</div>;
    }

    componentDidMount() {
        console.log('componentDidMount called');
    }
}

export default MountExample;

在挂载阶段,首先调用 constructor 进行初始化操作,然后 componentWillMount 在渲染之前调用(虽然这个方法在 React 17 及以后版本已被弃用),接着 render 方法返回需要渲染的 JSX,最后 componentDidMount 在组件挂载到 DOM 后调用,常用于数据获取、事件绑定等操作。

更新阶段

import React, { Component } from'react';

class UpdateExample extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
        this.increment = this.increment.bind(this);
    }

    componentWillReceiveProps(nextProps) {
        console.log('componentWillReceiveProps called with new props:', nextProps);
    }

    shouldComponentUpdate(nextProps, nextState) {
        // 这里可以根据 nextProps 和 nextState 判断是否需要更新
        return true;
    }

    componentWillUpdate(nextProps, nextState) {
        console.log('componentWillUpdate called with new props and state:', nextProps, nextState);
    }

    increment() {
        this.setState({
            count: this.state.count + 1
        });
    }

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

    componentDidUpdate(prevProps, prevState) {
        console.log('componentDidUpdate called with prev props and state:', prevProps, prevState);
    }
}

export default UpdateExample;

在更新阶段,当 propsstate 发生变化时,componentWillReceiveProps(已弃用)会在接收到新 props 时调用,shouldComponentUpdate 可以用于性能优化,决定组件是否需要更新,componentWillUpdate(已弃用)在组件更新前调用,render 方法再次渲染组件,componentDidUpdate 在组件更新后调用。

卸载阶段

import React, { Component } from'react';

class UnmountExample extends Component {
    constructor(props) {
        super(props);
    }

    componentWillUnmount() {
        console.log('componentWillUnmount called');
    }

    render() {
        return <div>Unmount example</div>;
    }
}

export default UnmountExample;

在卸载阶段,componentWillUnmount 方法会在组件从 DOM 中移除时调用,可用于清理资源,如取消定时器、解绑事件等操作。

生命周期区别的本质

类组件的生命周期方法是基于面向对象编程的设计,通过一系列的钩子函数,让开发者可以在组件不同的生命阶段执行特定的操作。这种方式虽然功能强大,但随着组件逻辑的复杂,生命周期方法之间的调用顺序和相互影响可能变得难以理解和维护。而函数组件通过 useEffect 模拟生命周期行为,采用的是函数式编程思想,将副作用操作分离出来,每个 useEffect 只关注一个特定的副作用,使得代码逻辑更加清晰,易于理解和维护。同时,useEffect 的依赖数组机制也提供了一种更灵活的控制副作用执行时机的方式。

代码结构和可读性

函数组件的代码结构与可读性

函数组件的代码结构通常较为简洁。它以函数的形式定义,接收 props 并返回 JSX。逻辑部分通过函数内部的变量和函数来实现,没有类的概念,也就不存在 this 上下文的问题。例如下面这个展示用户信息的函数组件:

import React from'react';

const UserInfo = ({ user }) => {
    const fullName = `${user.firstName} ${user.lastName}`;

    const handleClick = () => {
        console.log(`Clicked on user: ${fullName}`);
    };

    return (
        <div>
            <p>{fullName}</p>
            <button onClick={handleClick}>View Details</button>
        </div>
    );
};

export default UserInfo;

在这个组件中,逻辑清晰明了。定义了一个计算用户全名的变量 fullName 和一个处理点击事件的函数 handleClick,然后在返回的 JSX 中使用这些变量和函数。整体代码结构简单,易于阅读和理解,特别适合只关注 UI 展示和简单交互逻辑的场景。

类组件的代码结构与可读性

类组件的代码结构相对复杂,因为它基于类的概念,包含构造函数、生命周期方法、实例方法等。以下是一个实现同样功能的类组件示例:

import React, { Component } from'react';

class UserInfoClass extends Component {
    constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
    }

    handleClick() {
        const fullName = `${this.props.user.firstName} ${this.props.user.lastName}`;
        console.log(`Clicked on user: ${fullName}`);
    }

    render() {
        const fullName = `${this.props.user.firstName} ${this.props.user.lastName}`;
        return (
            <div>
                <p>{fullName}</p>
                <button onClick={this.handleClick}>View Details</button>
            </div>
        );
    }
}

export default UserInfoClass;

在这个类组件中,需要在构造函数中绑定 handleClick 方法,以确保 this 上下文的正确性。render 方法中也需要计算 fullName,虽然功能与函数组件相同,但代码结构上显得更为繁琐。而且类组件中 this 的使用可能会导致一些难以调试的问题,例如忘记绑定方法或者在不同作用域下 this 指向错误等。

代码结构和可读性区别的本质

函数组件的简洁性源于其函数式编程的风格,将组件逻辑视为数据到 UI 的映射,没有类的复杂结构和 this 上下文的困扰,使得代码更易于理解和维护,尤其适合简单的、纯展示性的组件。而类组件的面向对象结构虽然提供了更强大的功能,如状态管理和生命周期控制,但也带来了更多的样板代码和潜在的 this 相关问题,在复杂组件中可能导致代码结构变得臃肿,可读性下降。对于大型项目,合理地选择函数组件和类组件,根据组件的功能复杂度来决定使用哪种方式,可以有效地提高代码的可维护性和可读性。

性能表现

函数组件的性能

函数组件本身在性能上并没有固有的优势或劣势,但通过 React Hook 进行状态管理和副作用处理,可以在一定程度上优化性能。例如,useStateuseEffect 的依赖数组机制可以精确控制状态更新和副作用的执行时机,避免不必要的重新渲染。

import React, { useState, useEffect } from'react';

const MemoizedComponent = () => {
    const [count, setCount] = useState(0);
    const [name, setName] = useState('');

    useEffect(() => {
        console.log('Effect called due to count or name change');
    }, [count, name]);

    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={() => setCount(count + 1)}>Increment Count</button>
            <input
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="Enter name"
            />
        </div>
    );
};

export default MemoizedComponent;

在上述代码中,useEffect 的依赖数组包含 countname,只有当这两个状态变量之一发生变化时,useEffect 中的副作用才会执行。如果依赖数组为空,副作用只会在组件挂载和卸载时执行,有效地减少了不必要的计算和渲染。

类组件的性能

类组件在性能方面可以通过 shouldComponentUpdate 方法进行优化。通过在 shouldComponentUpdate 中比较 prevPropsnextPropsprevStatenextState,决定组件是否需要更新。

import React, { Component } from'react';

class OptimizedClassComponent extends Component {
    constructor(props) {
        super(props);
        this.state = {
            count: 0
        };
        this.increment = this.increment.bind(this);
    }

    shouldComponentUpdate(nextProps, nextState) {
        // 这里可以根据具体业务逻辑进行比较
        return nextState.count!== this.state.count;
    }

    increment() {
        this.setState({
            count: this.state.count + 1
        });
    }

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

export default OptimizedClassComponent;

在这个类组件中,shouldComponentUpdate 方法只在 count 状态变化时返回 true,表示组件需要更新,否则阻止更新,从而提高性能。但这种方式需要开发者手动比较 propsstate,如果逻辑复杂,代码可能会变得难以维护。

性能表现区别的本质

函数组件借助 React Hook 的依赖数组机制,以一种声明式的方式控制副作用和重新渲染,相对简洁且不易出错。而类组件通过 shouldComponentUpdate 进行性能优化,需要手动编写比较逻辑,虽然灵活性高,但在复杂场景下容易出现错误,且维护成本较高。在实际应用中,对于简单组件,函数组件的性能优化方式可能更易于实现和理解;而对于复杂组件,类组件的 shouldComponentUpdate 可以提供更精细的控制,但需要更多的开发精力来确保其正确性。

高阶组件与 Hook 的使用差异

函数组件与 Hook

Hook 为函数组件提供了一种在不编写类的情况下使用状态和其他 React 特性的方式。例如,useReducer 是一个类似于 useState 的 Hook,但适用于更复杂的状态逻辑,它基于 Redux 的 reducer 概念。

import React, { useReducer } from'react';

const initialState = {
    count: 0
};

const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return { count: state.count + 1 };
        case 'decrement':
            return { count: state.count - 1 };
        default:
            return state;
    }
};

const CounterReducer = () => {
    const [state, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            <p>Count: {state.count}</p>
            <button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
            <button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
        </div>
    );
};

export default CounterReducer;

在这个函数组件中,通过 useReducer 管理复杂的状态逻辑,dispatch 函数用于触发 reducer 中的不同操作。Hook 的使用使得函数组件能够轻松处理复杂状态,同时保持代码的简洁性。

类组件与高阶组件

高阶组件(HOC)是 React 中用于复用组件逻辑的一种高级技巧。它是一个函数,接收一个组件作为参数,并返回一个新的组件。以下是一个简单的 HOC 示例,用于给组件添加日志功能:

import React, { Component } from'react';

const withLogging = (WrappedComponent) => {
    return class extends Component {
        componentWillMount() {
            console.log('Component will mount');
        }

        componentDidMount() {
            console.log('Component did mount');
        }

        componentWillUnmount() {
            console.log('Component will unmount');
        }

        render() {
            return <WrappedComponent {...this.props} />;
        }
    };
};

class MyComponent extends Component {
    render() {
        return <div>My Component</div>;
    }
}

const LoggedComponent = withLogging(MyComponent);

export default LoggedComponent;

在这个例子中,withLogging 是一个高阶组件,它接收一个组件 WrappedComponent,并返回一个新的类组件,在新组件的生命周期方法中添加了日志功能。

高阶组件与 Hook 使用差异的本质

Hook 是 React 提供的一种在函数组件中复用状态逻辑的方式,基于函数式编程思想,更加简洁直观,且可以在组件内部灵活使用,每个 Hook 专注于一个特定的功能,如状态管理(useStateuseReducer)、副作用处理(useEffect)等。而高阶组件是基于面向对象编程的一种组件复用技巧,通过包裹组件的方式添加额外的功能,虽然功能强大,但可能会导致组件层级嵌套过深,增加调试和理解的难度。在实际开发中,Hook 更适合在函数组件内部进行细粒度的逻辑复用,而高阶组件在需要对整个组件进行功能增强时更为适用,两者各有其适用场景。

对 TypeScript 的支持

函数组件与 TypeScript

函数组件在使用 TypeScript 时,类型定义相对简洁。对于 props 的类型定义,可以使用接口或类型别名。例如:

import React from'react';

interface UserProps {
    name: string;
    age: number;
}

const UserComponent: React.FC<UserProps> = ({ name, age }) => {
    return (
        <div>
            <p>{name} is {age} years old</p>
        </div>
    );
};

export default UserComponent;

在上述代码中,通过接口 UserProps 定义了 props 的类型,React.FC 是 React 函数组件的类型别名,明确了组件接收的 props 类型。同时,函数组件内部的变量和函数的类型可以根据上下文自动推断,使得代码更加简洁明了。

类组件与 TypeScript

类组件在 TypeScript 中的类型定义相对复杂一些。除了需要定义 props 的类型,还需要处理 state 的类型以及类方法的类型。例如:

import React, { Component } from'react';

interface UserClassProps {
    name: string;
}

interface UserClassState {
    age: number;
}

class UserClassComponent extends Component<UserClassProps, UserClassState> {
    constructor(props: UserClassProps) {
        super(props);
        this.state = {
            age: 0
        };
    }

    incrementAge = () => {
        this.setState((prevState) => ({
            age: prevState.age + 1
        }));
    };

    render() {
        return (
            <div>
                <p>{this.props.name} is {this.state.age} years old</p>
                <button onClick={this.incrementAge}>Increment Age</button>
            </div>
        );
    }
}

export default UserClassComponent;

在这个类组件中,通过接口分别定义了 propsstate 的类型,构造函数和实例方法也需要明确其参数和返回值的类型。相比函数组件,类组件的类型定义涉及更多的方面,代码量相对较大。

对 TypeScript 支持区别的本质

函数组件的简洁性在 TypeScript 中同样体现,其函数式的结构使得类型定义更加直观,易于理解和维护。而类组件基于面向对象的结构,在 TypeScript 中需要全面考虑类的各个方面的类型,包括 propsstate 和方法,这虽然提供了更严格的类型检查,但也增加了代码的复杂性。对于注重类型安全性且逻辑相对简单的组件,函数组件结合 TypeScript 可以快速实现;而对于复杂的、具有较多状态和方法的组件,类组件在 TypeScript 中的类型定义虽然繁琐,但能提供更全面的类型保障。

社区和生态系统的倾向

函数组件在社区的流行趋势

随着 React Hook 的推出,函数组件在 React 社区中越来越受欢迎。其简洁的语法、函数式编程风格以及与 React 新特性的良好结合,使得它成为很多开发者的首选。许多新的 React 库和工具都更倾向于支持函数组件,例如 React Router v6 对函数组件的支持更加友好,提供了 useNavigateuseParams 等 Hook 来处理路由相关的功能。同时,函数组件在代码复用和测试方面的优势也得到了社区的广泛认可。在测试函数组件时,可以像测试普通函数一样,直接传入 props 并验证返回的 JSX,无需处理类组件中 this 上下文等复杂问题。

类组件在生态系统中的地位

尽管函数组件逐渐流行,但类组件在 React 生态系统中仍然有其地位。一些早期的大型 React 项目可能仍然基于类组件构建,由于代码迁移成本较高,这些项目可能会继续维护和扩展类组件的代码。此外,对于一些需要深入理解和使用 React 生命周期机制的场景,类组件的完整生命周期方法提供了更直接的控制方式。不过,随着 React 的发展,类组件的使用逐渐减少,社区对类组件的更新和支持也相对有限,更多的精力被投入到函数组件和相关 Hook 的优化与扩展上。

社区和生态系统倾向区别的本质

社区对函数组件的倾向源于其符合现代前端开发追求简洁、高效和可维护性的趋势。函数组件能够更轻松地与新的 React 特性融合,并且在代码复用、测试等方面具有天然的优势。而类组件虽然功能强大,但复杂的结构和面向对象的编程方式在一定程度上增加了开发和维护的难度。生态系统的发展也推动了函数组件的流行,新的库和工具为了迎合主流开发方式,更倾向于支持函数组件。不过,类组件在特定场景下的历史价值和功能完整性,使得它在 React 生态系统中仍占有一席之地,只是随着时间推移,其使用比例会逐渐降低。