Skip to content

22类组件与函数组件的对比及React测试实践

一、类组件与函数组件的优缺点对比

React组件主要分为类组件(Class Component)和函数组件(Functional Component),两者在语法、功能和适用场景上有显著差异,选择需结合项目需求和开发效率。

1. 类组件(Class Component)

定义:基于ES6 class语法,继承React.Component,通过render()方法返回JSX。

优点

  • 完整的生命周期控制:通过componentDidMountcomponentDidUpdate 等生命周期方法,可精确控制组件在挂载、更新、卸载等阶段的行为,适合处理复杂的副作用逻辑(如订阅事件、清理资源)。
  • 内置状态管理:通过this.statethis.setState管理组件状态,早期React中是唯一支持状态的组件类型。
  • 成熟的逻辑组织方式:对于复杂组件(如包含多个状态和方法),类的封装性可使代码结构清晰(如按功能划分方法)。

缺点

  • 代码冗余:需要编写classconstructorrender等模板代码,不够简洁。
  • this绑定问题:类方法中的this默认指向undefined,需手动绑定(如this.handleClick = this.handleClick.bind(this) )或使用箭头函数,易出错。
  • 逻辑复用复杂:复用状态逻辑需通过高阶组件(HOC)或render props,可能导致“嵌套地狱”。
  • 测试难度较高:类组件依赖实例,测试时需模拟this和生命周期,复杂度高于函数组件。

2. 函数组件(Functional Component)

定义:以函数形式定义,直接返回JSX,结合Hooks(useStateuseEffect等)实现状态管理和副作用。

优点

  • 代码简洁:无需模板代码,逻辑一目了然,减少样板代码(如无需classrender)。
  • this问题:函数组件中无this关键字,避免绑定问题,降低认知成本。
  • 逻辑复用简单:通过自定义Hooks轻松复用状态逻辑(如useAuthuseFetch),避免HOC的嵌套问题。
  • 易于测试:函数组件本质是纯函数(输入props输出JSX),测试时可直接调用,无需模拟实例。
  • 更好的Tree-Shaking:函数组件在打包时更易被静态分析,未使用的组件可被移除,减少打包体积。

缺点

  • 复杂逻辑可能碎片化:多个Hooks在同一组件中使用时,逻辑可能分散(如多个useEffect),需合理组织(如拆分为自定义Hooks)。
  • 生命周期模拟需适应Hooks:需理解useEffect与类组件生命周期的对应关系(如useEffect(fn, [])对应componentDidMount ),初期有学习成本。

3. 适用场景总结

组件类型适用场景不适用场景
类组件复杂生命周期管理、旧项目维护追求简洁代码、逻辑复用频繁、新项目开发
函数组件新项目开发、逻辑复用需求高、简洁UI组件依赖复杂生命周期且难以用Hooks替代的场景(极少)

现状:函数组件+ Hooks已成为React开发的主流,React官方也推荐优先使用函数组件。

二、React测试工具与实践

React测试的核心目标是验证组件行为是否符合预期,常用工具组合为Jest(测试运行器+断言库)和React Testing Library (组件测试库),两者配合可高效测试组件功能。

1. 测试工具基础:Jest + React Testing Library

1.1 工具定位

  • Jest:负责执行测试用例、提供断言方法(如expect().toBe())、模拟依赖(如jest.mock())、生成测试报告。
  • React Testing Library:专注于模拟用户行为(如点击、输入),提供查询DOM元素的方法,强调“测试组件的实际使用方式”而非实现细节。

1.2 基本使用流程

安装依赖

bash
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event

示例:测试一个简单按钮组件

tsx
// Button.tsx:待测试组件
const Button = ({label, onClick}) => {
    return <button onClick={onClick}>{label}</button>;
};

// Button.test.tsx:测试用例
import {render, screen} from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import Button from './Button';

// 测试渲染和点击事件
test('渲染按钮并触发点击事件', async () => {
    // 1. 准备:模拟点击回调
    const handleClick = jest.fn();

    // 2. 渲染组件
    render(<Button label="点击我" onClick={handleClick}/>);

    // 3. 验证:按钮是否渲染(查询元素)
    const button = screen.getByText('点击我');
    expect(button).toBeInTheDocument(); // 断言:按钮在DOM中

    // 4. 模拟用户行为:点击按钮
    await userEvent.click(button);

    // 5. 验证:回调是否被调用
    expect(handleClick).toHaveBeenCalledTimes(1);
});

核心API

  • render(component):渲染组件到虚拟DOM。
  • screen:提供查询方法(如getByTextgetByRole),用于获取DOM元素。
  • userEvent:模拟真实用户交互(如clicktype),比fireEvent更贴近实际行为。

2. 单元测试与集成测试的区别

2.1 单元测试(Unit Testing)

  • 目标:测试独立的组件、函数或Hook,隔离外部依赖(如其他组件、API请求)。
  • 特点:速度快、覆盖细,聚焦单一功能点,依赖模拟(mock)外部资源。
  • 示例:测试一个计数器组件的+1按钮是否正确更新状态(不依赖父组件)。
tsx
// Counter.tsx
const Counter = () => {
    const [count, setCount] = useState(0);
    return (
        <div>
            <span>{count}</span>
            <button onClick={() => setCount(c => c + 1)}>+1</button>
        </div>
    );
};

// Counter.test.tsx(单元测试)
test('点击+1按钮后计数增加', async () => {
    render(<Counter/>);
    const countDisplay = screen.getByText('0');
    const incrementBtn = screen.getByText('+1');

    await userEvent.click(incrementBtn);
    expect(countDisplay).toHaveTextContent('1'); // 验证计数更新
});

2.2 集成测试(Integration Testing)

  • 目标:测试多个组件的交互逻辑,验证它们协同工作时是否符合预期。
  • 特点:更接近用户实际使用场景,覆盖组件间数据传递、状态共享等。
  • 示例:测试“表单输入→提交按钮→列表展示”的完整流程(涉及表单组件、按钮组件、列表组件)。
tsx
// 集成测试:测试表单提交后列表更新
test('表单提交后新增项显示在列表中', async () => {
    render(
        <div>
            <InputForm/> {/* 输入框+提交按钮 */}
            <ItemList/> {/* 展示列表 */}
        </div>
    );

    const input = screen.getByRole('textbox');
    const submitBtn = screen.getByText('提交');

    // 模拟用户输入并提交
    await userEvent.type(input, '新项');
    await userEvent.click(submitBtn);

    // 验证列表是否新增项
    expect(screen.getByText('新项')).toBeInTheDocument();
});

2.3 选择策略

  • 单元测试:覆盖工具函数、自定义Hooks、独立UI组件(如按钮、输入框)。
  • 集成测试:覆盖核心业务流程(如登录、购物车操作),确保组件协同工作正常。

3. 测试Hooks与异步逻辑

3.1 测试自定义Hooks

需使用@testing-library/react-hooks库,通过renderHook渲染Hooks并获取其返回值。

示例:测试useCounter Hook

tsx
// useCounter.ts
const useCounter = (initialValue = 0) => {
    const [count, setCount] = useState(initialValue);
    const increment = () => setCount(c => c + 1);
    return {count, increment};
};

// useCounter.test.ts
import {renderHook, act} from '@testing-library/react-hooks';
import useCounter from './useCounter';

test('初始值为0,increment后变为1', () => {
    // 渲染Hook
    const {result} = renderHook(() => useCounter());

    // 初始状态验证
    expect(result.current.count).toBe(0);

    // 模拟调用increment
    act(() => {
        result.current.increment();
    });

    // 更新后状态验证
    expect(result.current.count).toBe(1);
});

3.2 测试异步逻辑

异步逻辑(如API请求)需使用async/await,配合findBy*查询(异步查询,等待元素出现)。

示例:测试异步数据加载组件

tsx
// UserProfile.tsx:异步加载用户数据
const UserProfile = ({userId}) => {
    const [user, setUser] = useState(null);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(data => {
                setUser(data);
                setLoading(false);
            });
    }, [userId]);

    if (loading) return <div>加载中...</div>;
    return <div>姓名:{user.name}</div>;
};

// UserProfile.test.tsx:测试异步加载
import {render, screen, waitFor} from '@testing-library/react';
import UserProfile from './UserProfile';

// 模拟fetch请求
beforeEach(() => {
    global.fetch = jest.fn(() =>
        Promise.resolve({
            json: () => Promise.resolve({id: 1, name: '张三'}),
        })
    );
});

test('加载完成后显示用户姓名', async () => {
    render(<UserProfile userId={1}/>);

    // 验证加载状态
    expect(screen.getByText('加载中...')).toBeInTheDocument();

    // 等待异步加载完成(两种方式)
    // 方式1:使用findBy*(异步查询,自动等待)
    const nameDisplay = await screen.findByText('姓名:张三');
    expect(nameDisplay).toBeInTheDocument();

    // 方式2:使用waitFor(手动等待断言成立)
    await waitFor(() => {
        expect(screen.getByText('姓名:张三')).toBeInTheDocument();
    });
});

三、总结

  • 组件选择:函数组件+ Hooks以简洁性和逻辑复用优势成为主流,类组件适合复杂生命周期场景但逐渐被替代。
  • 测试实践
    • 用Jest + React Testing Library模拟用户行为,测试组件实际功能而非实现细节;
    • 单元测试覆盖独立组件和Hooks,集成测试验证组件协同流程;
    • 测试Hooks用@testing-library/react-hooks,异步逻辑需配合async/awaitfindBy*/waitFor

合理的组件设计和测试策略能显著提升React项目的可维护性和稳定性,减少线上bug。