React中的Class和Function组件简介

React 组件现代化演进:Class 组件 vs. 函数组件深度剖析报告

摘要

在现代 React 开发中,函数组件(Functional Components)配合 Hooks 是绝对的主流和官方推荐的写法。Class 组件虽然并未被废弃,并且在某些特定场景下仍有其用武之地(如错误边界),但对于所有新项目和新组件的开发,都应优先选择函数组件。

1. 简介:两种组件范式

在 React 的世界里,组件是构建用户界面的核心单元。长久以来,我们有两种主要的方式来创建组件:

  1. Class 组件 (Class Components) :基于 ES6 的 class 语法,继承自 React.Component,拥有自己的实例(this)、状态(state)和生命周期方法。
  2. 函数组件 (Functional Components) :最初是简单的 JavaScript 函数,仅接收 props 并返回 JSX,被称为“无状态组件”。自 React 16.8 引入 Hooks 之后,函数组件获得了管理状态、处理副作用等能力,变得与 Class 组件一样强大,甚至更灵活。

2. Class组件深度解析

Class 组件是 React 早期的核心,其设计思想深受面向对象编程(OOP)的影响。

import React, { Component } from 'react';

class MyClassComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
    // 为了在回调函数中正确使用 `this`,需要手动绑定
    this.handleClick = this.handleClick.bind(this);
  }

  componentDidMount() {
    console.log('组件已挂载');
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.count !== this.state.count) {
      console.log('count 状态已更新');
    }
  }

  componentWillUnmount() {
    console.log('组件将卸载');
  }

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

  render() {
    return (
      <div>
        <h1>Class 组件</h1>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>点击增加</button>
      </div>
    );
  }
}

优点

  • 成熟稳定 :作为 React 早期的标准,拥有大量成熟的文档、教程和第三方库支持。
  • 生命周期方法明确componentDidMount, shouldComponentUpdate 等方法命名直观,对于有 OOP 背景的开发者来说,其执行时机和作用一目了然。
  • 支持特定功能 :目前,Error Boundaries(错误边界)和 getSnapshotBeforeUpdate 这两个生命周期方法 仍然只能在 Class 组件中使用

缺点

  • 语法冗余 :需要编写 constructor, super(props),并且方法需要手动绑定 this(或使用 class fields 语法),代码量相对较多。
  • this 指向混乱this 在 JavaScript 中是一个复杂的概念。在 Class 组件中,你必须时刻关注 this 的指向,尤其是在事件处理函数和回调中,这常常是初学者出错的重灾区。
  • 逻辑复用困难
    • HOCs (高阶组件)Render Props 是 Class 组件时代复用逻辑的主要模式,但这两种模式都容易导致“组件嵌套地狱 (Wrapper Hell)”,使得 React DevTools 中的组件层级变得非常深,难以调试。
    • 相关的业务逻辑被迫分散在不同的生命周期方法中。例如,一个订阅功能的逻辑可能需要在 componentDidMount 中开始订阅,在 componentWillUnmount 中取消订阅,逻辑被割裂。
  • 不利于性能优化 :Class 组件的实例较大,且其内部方法的复杂性使得编译器(如 Babel)和打包工具(如 Webpack)难以进行有效的优化和摇树(Tree Shaking)。

3. 函数组件与Hooks深度解析

函数组件以其简洁性著称,而在 Hooks 出现后,它便如虎添翼,成为了现代 React 开发的首选。

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

function MyFunctionalComponent(props) {
  const [count, setCount] = useState(0);

  // useEffect 组合了 componentDidMount, componentDidUpdate, componentWillUnmount 的功能
  useEffect(() => {
    console.log('组件已挂载或 count 已更新');

    // 返回一个函数,这个函数会在组件卸载时执行
    return () => {
      console.log('组件将卸载');
    };
  }, [count]); // 依赖数组,仅在 count 变化时重新执行 effect

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

  return (
    <div>
      <h1>函数组件</h1>
      <p>Count: {count}</p>
      <button onClick={handleClick}>点击增加</button>
    </div>
  );
}

优点

  • 语法简洁,可读性高 :没有 this,没有 class 关键字,没有 render 方法。代码更少,意图更清晰,心智负担更小。
  • 状态逻辑分离 :通过 useState,可以将不同的状态逻辑分离开,而不是全部堆在一个巨大的 this.state 对象中。
  • 强大的逻辑复用机制(自定义 Hooks) :这是函数组件 最核心的优势。你可以将组件逻辑(如状态管理、副作用)提取到可重用的函数中(自定义 Hook),轻松地在不同组件间共享,而不会增加组件嵌套。这完美解决了 HOC 和 Render Props 的“嵌套地狱”问题。
  • 按关注点分离逻辑useEffect 允许你将相关的逻辑组织在一起。例如,一个获取数据和处理其副作用的逻辑可以完整地放在一个 useEffect 块中,而不是分散在 componentDidMountcomponentDidUpdate 里。
  • 更易于优化和未来发展 :函数组件的“纯函数”特性使得 React 团队能更好地对其进行编译时优化。同时,React 的前沿特性,如并发模式(Concurrent Mode)和服务器组件(Server Components),都是围绕函数组件和 Hooks 构建的。

缺点

  • 学习曲线 :对于习惯了 OOP 和生命周期的开发者来说,Hooks 的心智模型(声明式、同步副作用)需要一个适应过程。
  • Hooks 的规则 :必须在函数组件的顶层调用 Hooks,不能在循环、条件或嵌套函数中调用。这需要 linter 插件来强制约束,否则容易出错。
  • useEffect 的依赖陷阱useEffect 的依赖数组(dependency array)是其强大之处,也是常见的 Bug 来源。忘记添加依赖、或在依赖中加入了不稳定的引用(如函数、对象)都可能导致无限循环或不符合预期的行为。

4. 对比总结

特性 Class组件 函数组件(with Hooks) 优胜者
语法 冗余,需要 class, constructor, this 简洁,就是 JavaScript 函数 函数组件
状态管理 this.state, this.setState() useState(), useReducer() 函数组件
生命周期/副作用 componentDidMount 等生命周期方法 useEffect(), useLayoutEffect() 函数组件
逻辑复用 HOCs, Render Props (易产生嵌套地狱) 自定义 Hooks (优雅、无嵌套) 函数组件
this 问题 普遍存在,需要手动处理 完全没有 this 函数组件
代码组织 逻辑按生命周期分散 逻辑按关注点聚合 函数组件
生态与未来 维护状态,不再是发展重点 React 团队的未来发展方向 函数组件
特殊场景 唯一支持 Error Boundaries 不支持 Class 组件

5. 结论与建议

函数组件与 Hooks 是 React 的现在与未来。

React 官方团队明确推荐在新代码中使用函数组件和 Hooks。这不仅仅是一种风格偏好,而是基于其在代码复用、可维护性、性能和未来适应性方面的巨大优势。

给开发者的建议:

  1. 新项目/新组件 :毫无疑问,始终使用函数组件和 Hooks
  2. 现有项目
    • 没有必要将一个稳定运行的大型 Class 组件项目立即重构成函数组件,这会带来不必要的风险和成本。
    • 可以在维护旧项目时,逐步将新增的功能或重构的小模块用函数组件实现。
    • 如果遇到需要使用 **错误边界(Error Boundary)**的场景,你仍然需要使用 Class 组件来包裹可能会出错的子组件树。
  3. 学习重点 :如果你是 React 新手,请将学习重心放在函数组件和 Hooks 上。理解 useState, useEffect, useContext 以及如何创建自定义 Hooks 是掌握现代 React 的关键。

总之,虽然 Class 组件依然是 React 的一部分,但函数组件已经凭借其无与伦比的优势,成为了现代化 React 开发的黄金标准。拥抱函数组件,就是拥抱一个更简洁、更灵活、更强大的 React。

当然!自定义 Hook 是函数组件最强大的特性之一,它能让你将组件逻辑提取到可重用的函数中。下面我将通过一个非常常见的场景——数据请求——来展示它的威力。

自定义Hook实例:useFetch

1. 问题背景:重复的逻辑

假设我们有两个组件:一个需要显示用户信息 (UserProfile),另一个需要显示产品列表 (ProductList)。它们都需要从 API 获取数据,并且在获取数据的过程中,都需要处理 加载中(loading)错误(error) 的状态。

如果不使用自定义 Hook,我们的代码可能会是这样:

组件一:UserProfile.js

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

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(`https://api.example.com/users/${userId}`)
      .then(res => {
        if (!res.ok) throw new Error('数据加载失败');
        return res.json();
      })
      .then(data => setUser(data))
      .catch(err => setError(err))
      .finally(() => setLoading(false));
  }, [userId]); // 当 userId 变化时,重新请求

  if (loading) return <p>正在加载用户信息...</p>;
  if (error) return <p>错误: {error.message}</p>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>邮箱: {user.email}</p>
    </div>
  );
}

组件二:ProductList.js

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

function ProductList() {
  const [products, setProducts] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    setLoading(true);
    fetch(`https://api.example.com/products`)
      .then(res => {
        if (!res.ok) throw new Error('数据加载失败');
        return res.json();
      })
      .then(data => setProducts(data))
      .catch(err => setError(err))
      .finally(() => setLoading(false));
  }, []); // 空数组表示只在组件挂载时请求一次

  if (loading) return <p>正在加载产品列表...</p>;
  if (error) return <p>错误: {error.message}</p>;

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

问题显而易见
UserProfileProductList 组件中,关于 useState 定义 loadingerror 状态,以及 useEffect 中执行 fetch、处理响应和错误的代码,几乎是完全一样的。这是典型的代码重复。

2. 解决方案:创建自定义Hook useFetch

现在,我们将这些重复的逻辑提取到一个名为 useFetch 的自定义 Hook 中。

自定义 Hook:useFetch.js

一个自定义 Hook 本质上就是一个名字以 use 开头的 JavaScript 函数,它可以在内部调用其他的 Hook(如 useState, useEffect)。

import { useState, useEffect } from 'react';

// 自定义 Hook,接收一个 url 作为参数
function useFetch(url) {
  // 1. 将所有共享的状态逻辑都放在这里
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  // 2. 将所有共享的副作用逻辑也放在这里
  useEffect(() => {
    // 使用 AbortController 来处理组件卸载时中断请求的场景,这是一个好习惯
    const controller = new AbortController();

    // 在每次请求前重置状态
    setLoading(true);
    setData(null);
    setError(null);

    fetch(url, { signal: controller.signal })
      .then(res => {
        if (!res.ok) {
          throw new Error('网络响应失败');
        }
        return res.json();
      })
      .then(data => {
        setData(data);
      })
      .catch(err => {
        // 如果是主动中断的错误,则不更新 error 状态
        if (err.name !== 'AbortError') {
          setError(err);
        }
      })
      .finally(() => {
        setLoading(false);
      });

    // 3. useEffect 的清理函数:在组件卸载或 url 变化时执行
    return () => {
      controller.abort();
    };
  }, [url]); // 依赖项是 url,当 url 变化时,useEffect 会重新执行

  // 4. 返回组件需要的所有状态和数据
  return { data, loading, error };
}

export default useFetch;

3. 应用自定义Hook:重构组件

现在,我们可以用这个 useFetch Hook 来极大地简化我们的组件代码。

重构后的 UserProfile.js

import React from 'react';
import useFetch from './useFetch'; // 引入自定义 Hook

function UserProfile({ userId }) {
  // 像使用 useState 一样,一行代码就获取了所有需要的状态!
  const { data: user, loading, error } = useFetch(`https://api.example.com/users/${userId}`);

  if (loading) return <p>正在加载用户信息...</p>;
  if (error) return <p>错误: {error.message}</p>;
  if (!user) return null; // 确保 user 存在

  return (
    <div>
      <h1>{user.name}</h1>
      <p>邮箱: {user.email}</p>
    </div>
  );
}

重构后的 ProductList.js

import React from 'react';
import useFetch from './useFetch'; // 同样引入自定义 Hook

function ProductList() {
  // 再次使用,只是 URL 不同
  const { data: products, loading, error } = useFetch(`https://api.example.com/products`);

  if (loading) return <p>正在加载产品列表...</p>;
  if (error) return <p>错误: {error.message}</p>;
  if (!products) return null; // 确保 products 存在

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

总结与优势

通过这个 useFetch 的例子,我们可以清晰地看到自定义 Hook 带来的好处:

  1. 逻辑复用 (Reusability) :数据请求的整套逻辑(状态管理 + 副作用)被封装在 useFetch 中,任何需要发起网络请求的组件都可以直接调用它,避免了重复编写相同的代码。

  2. 关注点分离 (Separation of Concerns) :组件(如 UserProfile)不再关心数据 如何 获取,它只关心获取的结果(data, loading, error)。数据获取的复杂逻辑被完全隔离出去,使得组件本身的代码更简洁,专注于 UI 渲染。

  3. 可维护性 (Maintainability) :如果将来需要修改数据请求的逻辑,比如:

    • fetch 更换为 axios
    • 统一添加 Authorization 请求头。
    • 增加全局的错误上报机制。
      只需要修改 useFetch.js 这一个文件,所有使用该 Hook 的组件都会自动获得更新,而无需逐一修改。

这就是自定义 Hook 的核心价值——它提供了一种极其优雅且强大的方式来组织和共享 React 组件中的有状态逻辑。

好的,除了数据请求(异步副作用),自定义 Hook 还可以用于封装各种各样的逻辑。下面我将提供三种不同类型的示例,分别涵盖:浏览器 API 交互UI 状态抽象性能优化

示例一:useLocalStorage - 封装浏览器API

这个 Hook 的目标是提供一个与 useState 体验完全一致的接口,但能够自动将状态持久化到浏览器的 LocalStorage 中。

1. 问题背景

我们经常需要在用户关闭或刷新页面后,仍然保留某些状态,比如网站的主题(暗色/亮色模式)、用户的表单输入等。传统的做法是:

  • useEffect 中使用 localStorage.setItem() 来保存状态。
  • useState 的初始值中通过 localStorage.getItem() 来读取状态。

这个逻辑会在每个需要持久化的组件中重复出现。

function ThemeSelector() {
  // 1. 读取初始值
  const savedTheme = localStorage.getItem('theme') || 'light';
  const [theme, setTheme] = useState(savedTheme);

  // 2. 当 theme 变化时,写入 LocalStorage
  useEffect(() => {
    localStorage.setItem('theme', theme);
  }, [theme]);

  // ... rest of the component
}

2. 自定义Hook实现:useLocalStorage.js

我们可以将上述逻辑封装起来。

import { useState, useEffect } from 'react';

// 一个帮助函数,用于安全地从 localStorage 读取数据
function getSavedValue(key, initialValue) {
  const savedValue = JSON.parse(localStorage.getItem(key));
  if (savedValue) return savedValue;

  // 如果初始值是一个函数,则执行它
  if (initialValue instanceof Function) return initialValue();
  
  return initialValue;
}

function useLocalStorage(key, initialValue) {
  // 使用 useState,但初始值从我们的帮助函数中获取
  const [value, setValue] = useState(() => {
    return getSavedValue(key, initialValue);
  });

  // 使用 useEffect,在 value 或 key 发生变化时,将新值写入 localStorage
  useEffect(() => {
    localStorage.setItem(key, JSON.stringify(value));
  }, [key, value]);

  // 返回与 useState 完全相同的数组
  return [value, setValue];
}

export default useLocalStorage;

3. 如何使用

现在,组件的代码变得极其简洁,就像使用普通的 useState 一样,但它自动获得了持久化的超能力。

import React from 'react';
import useLocalStorage from './useLocalStorage';

function ThemeSelector() {
  // 一行代码搞定状态声明和持久化!
  const [theme, setTheme] = useLocalStorage('theme', 'light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <div style={{ background: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff' }}>
      <h1>当前主题是: {theme}</h1>
      <button onClick={toggleTheme}>切换主题</button>
    </div>
  );
}

示例二:useToggle - 抽象UI状态逻辑

这个 Hook 的目标是简化常见的布尔值(true/false)切换逻辑,例如控制模态框的显示/隐藏、菜单的展开/折叠等。

1. 问题背景

在一个组件中,我们通常这样写:

function MyModal() {
  const [isOpen, setIsOpen] = useState(false);

  const openModal = () => setIsOpen(true);
  const closeModal = () => setIsOpen(false);
  const toggleModal = () => setIsOpen(prev => !prev);

  // ...
}

虽然简单,但 useStatesetIsOpen 的调用还是有些模板化。

2. 自定义Hook实现:useToggle.js

我们可以把这个切换逻辑封装成一个更具表达力的 Hook。

import { useState, useCallback } from 'react';

function useToggle(initialState = false) {
  const [state, setState] = useState(initialState);

  // 使用 useCallback 避免在组件重渲染时重复创建函数
  const toggle = useCallback(() => {
    setState(prevState => !prevState);
  }, []);

  // 你也可以返回一个对象,提供更丰富的 API
  // return { state, toggle, setTrue: () => setState(true), setFalse: () => setState(false) };

  return [state, toggle];
}

export default useToggle;

3. 如何使用

组件代码变得更加声明式和简洁。

import React from 'react';
import useToggle from './useToggle';

function CollapsibleContent() {
  // `isExpanded` 是状态,`toggleExpanded` 是切换函数
  const [isExpanded, toggleExpanded] = useToggle(false);

  return (
    <div>
      <button onClick={toggleExpanded}>
        {isExpanded ? '收起' : '展开'}
      </button>
      {isExpanded && (
        <p>
          这是一些可以折叠的内容。自定义 Hook 让状态管理变得如此简单!
        </p>
      )}
    </div>
  );
}

示例三:useDebounce - 性能优化

这个 Hook 用于创建一个“防抖”的值。当某个值频繁变化时(例如用户在搜索框中快速输入),我们只希望在它停止变化一段时间后再使用这个值(例如发起 API 请求),以避免不必要的性能开销。

1. 问题背景

在一个搜索组件中,如果每次输入都去请求 API,会造成大量的冗余请求。我们需要等待用户停止输入后再行动。

function SearchBar() {
  const [searchTerm, setSearchTerm] = useState('');

  useEffect(() => {
    // 每次 searchTerm 变化都会立即触发
    // api.search(searchTerm); // 性能灾难!
    
    // 手动实现 debounce
    const handler = setTimeout(() => {
      // api.search(searchTerm);
      console.log(`(模拟)搜索: ${searchTerm}`);
    }, 500);

    return () => {
      clearTimeout(handler);
    };
  }, [searchTerm]);

  // ...
}

这个 setTimeoutclearTimeout 的逻辑也是可以被完美复用的。

2. 自定义Hook实现:useDebounce.js

import { useState, useEffect } from 'react';

// 接收一个需要防抖的值和延迟时间
function useDebounce(value, delay) {
  // 创建一个 state 来存储防抖后的值
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // 设置一个定时器,在 delay 毫秒后更新 debouncedValue
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // 清理函数:
    // 如果 value 或 delay 在定时器结束前变化了,就取消上一个定时器
    // 这正是防抖的核心
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]); // 仅在 value 或 delay 变化时重新设置定时器

  return debouncedValue;
}

export default useDebounce;

3. 如何使用

组件现在只需要关心原始输入值和防抖后的值,逻辑非常清晰。

import React, { useState, useEffect } from 'react';
import useDebounce from './useDebounce';

function LiveSearch() {
  const [inputValue, setInputValue] = useState('');
  
  // 使用 useDebounce Hook,延迟 500ms
  const debouncedSearchTerm = useDebounce(inputValue, 500);

  // 这个 useEffect 只会在用户停止输入 500ms 后执行
  useEffect(() => {
    if (debouncedSearchTerm) {
      console.log(`正在向 API 发送搜索请求: "${debouncedSearchTerm}"`);
      // fetch(`/api/search?q=${debouncedSearchTerm}`);
    }
  }, [debouncedSearchTerm]); // 依赖于防抖后的值

  return (
    <div>
      <input
        type="text"
        placeholder="输入以搜索..."
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
      />
      <p>实时输入: {inputValue}</p>
      <p>防抖后的值: {debouncedSearchTerm}</p>
    </div>
  );
}

这些例子展示了自定义 Hook 的巨大灵活性,它不仅限于某种特定类型的逻辑,而是任何你发现自己在组件间重复编写的有状态逻辑,都可以被提炼成一个简洁、可复用、可测试的自定义 Hook。