Skip to content

函数组件和类组件的本质区别究竟是什么?

React 开发中,我们总会遇到一个基础却核心的选择题:使用函数组件(Function Component)还是类组件(Class Component)?

随着 Hooks 的普及,许多开发者会直观地认为:“函数组件现在也能管理 state 和处理副作用了,所以它和类组件差不多,而且写起来更简单。” 这个结论没错,但它只触及了表面。函数组件和类组件的根本差异,并非仅仅是语法上的简洁或复杂,而是植根于两种截然不同的设计哲学和心智模型。

理解这一层差异,能帮助我们写出更健壮、更可预测的 React 代码。

核心差异:心智模型与数据流

我们可以从两种组件分别代表的编程范式来理解它们的根本不同。

类组件:面向对象的“实例”思维

类组件是面向对象编程(OOP)思想在 React 中的体现。当我们定义一个类组件时,我们实际上是在创建一个蓝图。在渲染时,React 会根据这个蓝图 new 一个组件实例。

这个实例是“活”的,它有自己的生命周期(componentDidMount, componentDidUpdate 等),并持有自己的内部状态(this.state)和方法(this.myMethod)。render 函数只是这个实例众多方法中的一个。无论何时,当我们想在组件内部访问 propsstate 时,我们总是通过 this 这个不稳定的指针来读取当前的值。

心智模型:我们操作的是一个长期存在的、状态可变的实例。

函数组件:函数式编程的“快照”思维

函数组件更贴近函数式编程(FP)的理念。它本质上就是一个普通的 JavaScript 函数,接收 props 作为参数,返回一段描述 UIJSX

它本身是无状态的。每次父组件更新,函数组件都会被重新调用,形成一次新的渲染。每一次渲染,都是一个独立的“快照”,函数内部的所有变量(包括通过 Hooks 获取的 state)都被固定在了那一刻。

那状态如何跨渲染周期持久化呢?答案是 HooksuseStateuseEffectHooks 让我们能够“钩入”到 React 自身的 state 管理机制中。React 帮助我们保管了 state,并在下一次渲染时,将最新的 state 值传递给重新执行的函数。

心智模型:我们操作的是一次性的渲染函数,它捕获了特定时刻的 propsstate

一个经典场景:PropsState 的“捕获”

实例快照这两种心智模型最大的区别,体现在对异步操作的处理上。我们来看一个经典的计数器例子。

假设有一个组件,它显示当前的计数值,并有一个按钮。点击按钮后,会在 3 秒后打印出当时的计数值。

类组件的实现

javascript
import React from 'react';

class ProfilePage extends React.Component {
  state = {
    count: 0,
  };

  logMessage = () => {
    setTimeout(() => {
      // 读取的是 3 秒后 this.state.count 的最新值
      alert(`Count is: ${this.state.count}`);
    }, 3000);
  };

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

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={this.handleClick}>Click me</button>
      </div>
    );
  }
}

操作流程:快速连续点击按钮 3 次。

结果:3 秒后,会弹出 3 个 alert,内容都是 Count is: 3

原因分析logMessage 方法通过 this.state.count 来读取计数值。this 是一个指向组件实例的可变引用。当 setTimeout 的回调函数在 3 秒后执行时,它读取的是那一刻 this.state 的最新值,而此时 count 早已变成了 3

函数组件的实现

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

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

  const logMessage = () => {
    setTimeout(() => {
      // 捕获了点击那一刻的 count 值
      alert(`Count is: ${count}`);
    }, 3000);
  };

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={handleClick}>Click me</button>
    </div>
  );
}

操作流程:同样,快速连续点击按钮 3 次。

结果3 秒后,会依次弹出 3alert,内容分别是 “Count is: 1”, “Count is: 2”, “Count is: 3”。

原因分析:这是由 JavaScript闭包特性决定的。当我们点击按钮时,handleClick 被调用。它调用 logMessage,此时的 logMessage 函数是在某一次特定渲染中定义的,它“捕获”了那次渲染作用域中的 count 值。

  • 第一次点击时,count0setCount(1) 被调用。同时,logMessage 捕获了 count = 0

  • 第二次点击时,组件重渲染,count1setCount(2) 被调用。同时,一个新的 logMessage 函数被创建,它捕获了 count = 1

以此类推。每个 setTimeout 的回调函数都持有自己被创建时 count 值的“快照”,因此打印出了符合我们直觉的结果。这种特性使得函数组件在处理异步逻辑时,行为更加可预测。

生命周期 vs. 副作用:不同的关注点

  • 类组件:以生命周期方法为中心,我们将逻辑分散到 componentDidMount(挂载时)、componentDidUpdate(更新时)、componentWillUnmount(卸载时)等方法中。这种方式的关注点是时间——组件在哪个生命阶段,就执行对应的逻辑。这常常导致相关联的逻辑被拆分在不同的方法里,不易维护。

  • 函数组件:以 useEffect 为中心,我们将相关的逻辑组织在一起。useEffect 的思维模式是:当某些数据(propsstate)发生变化时,我需要同步执行某些副作用。它的关注点是数据同步,而不是时间点。这使得我们可以将“数据获取和后续清理”、“事件监听和移除”等相关逻辑聚合在同一个 useEffect 块中,代码内聚性更强。

总结:我们应该如何选择?

回到最初的问题,函数组件和类组件的本质区别在于:

类组件是基于可变实例和生命周期的,其行为更像一个长期存活的对象;而函数组件是基于函数调用和闭包捕获的,其行为更像在特定时间点渲染出的快照。

在现代 React 开发中,我们应该如何选择?

对于新项目或新功能,优先并推荐使用函数组件和 Hooks。这是 React 官方主推的范式,它带来了诸多优势:

  1. 代码更简洁:省去了 constructorthis 绑定等样板代码。

  2. 逻辑复用更优雅:通过自定义 Hooks,可以轻松地抽离和复用带状态的逻辑,远比高阶组件(HOC)和 Render Props 模式更直观。

  3. 心智负担更小:避免了 this 指向在 JavaScript 中的复杂性。

  4. 行为更可预测:如前所述,闭包带来的值捕获特性让异步操作更符合直觉。

当然,这并不意味着类组件已经过时。在维护庞大的老旧项目时,我们依然会大量接触到类组件。因此,理解它的工作原理和心智模型,对于成为一名全面的 React 开发者来说,依然至关重要。

最终,选择哪种方式不仅是技术选型,更是选择一种更符合现代 React 开发范式的心智模型。拥抱函数组件,就是拥抱一个更简洁、更函数式的未来。

不知道说啥了很无语了