4.函数组件和类组件的本质区别究竟是什么? 来源:https://w0hog67yl81.feishu.cn/wiki/RsRVwEN1aiLdzJkytp4cVh47nNg 采集状态:已完成正文采集;正文滚动至底部,按块及代码行号采集 ​ 在 React 开发中,我们总会遇到一个基础却核心的选择题:使用函数组件(Function Component)还是类组件(Class Component)?​ ​ 随着 Hooks 的普及,许多开发者会直观地认为:“函数组件现在也能管理 state 和处理副作用了,所以它和类组件差不多,而且写起来更简单。” 这个结论没错,但它只触及了表面。函数组件和类组件的根本差异,并非仅仅是语法上的简洁或复杂,而是植根于两种截然不同的设计哲学和心智模型。​ ​ 理解这一层差异,能帮助我们写出更健壮、更可预测的 React 代码。​ ​ 核心差异:心智模型与数据流​ ​ 我们可以从两种组件分别代表的编程范式来理解它们的根本不同。​ ​ • 类组件:面向对象的“实例”思维​ 类组件是面向对象编程(OOP)思想在 React 中的体现。当我们定义一个类组件时,我们实际上是在创建一个蓝图。在渲染时,React 会根据这个蓝图 new 一个组件实例。 这个**实例**是“活”的,它有自己的生命周期(componentDidMount, componentDidUpdate 等),并持有自己的内部状态(this.state)和方法(this.myMethod)。render 函数只是这个实例众多方法中的一个。无论何时,当我们想在组件内部访问 props 或 state 时,我们总是通过 this 这个不稳定的指针来读取**当前**的值。 心智模型:我们操作的是一个长期存在的、状态可变的实例。 • 函数组件:函数式编程的“快照”思维​ 函数组件更贴近函数式编程(FP)的理念。它本质上就是一个普通的 JavaScript 函数,接收 props 作为参数,返回一段描述 UI 的 JSX。 它本身是无状态的。每次父组件更新,函数组件都会被**重新调用**,形成一次新的渲染。每一次渲染,都是一个独立的“快照”,函数内部的所有变量(包括通过 Hooks 获取的 state)都被固定在了那一刻。 那状态如何跨渲染周期持久化呢?答案是 Hooks。useState、useEffect 等 Hooks 让我们能够“钩入”到 React 自身的 state 管理机制中。React 帮助我们保管了 state,并在下一次渲染时,将最新的 state 值传递给重新执行的函数。 心智模型:我们操作的是一次性的渲染函数,它捕获了特定时刻的 props 和 state。 一个经典场景:Props 与 State 的“捕获”​ ​ “实例”和“快照”这两种心智模型最大的区别,体现在对异步操作的处理上。我们来看一个经典的计数器例子。​ ​ 假设有一个组件,它显示当前的计数值,并有一个按钮。点击按钮后,会在 3 秒后打印出当时的计数值。​ ​ 类组件的实现​ ​ 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 (

You clicked {this.state.count} times

); } } ​ 操作流程:快速连续点击按钮 3 次。​ 结果:3 秒后,会弹出 3 个 alert,内容都是 “Count is: 3”。​ ​ 原因分析:logMessage 方法通过 this.state.count 来读取计数值。this 是一个指向组件实例的可变引用。当 setTimeout 的回调函数在 3 秒后执行时,它读取的是**那一刻** this.state 的最新值,而此时 count 早已变成了 3。​ ​ 函数组件的实现​ ​ 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 (

You clicked {count} times

); } ​ 操作流程:同样,快速连续点击按钮 3 次。​ 结果:3 秒后,会依次弹出 3 个 alert,内容分别是 “Count is: 1”, “Count is: 2”, “Count is: 3”。​ ​ 原因分析:这是由 JavaScript 的**闭包**特性决定的。当我们点击按钮时,handleClick 被调用。它调用 logMessage,此时的 logMessage 函数是在某一次特定渲染中定义的,它“捕获”了那次渲染作用域中的 count 值。​ ​ • 第一次点击时,count 是 0,setCount(1) 被调用。同时,logMessage 捕获了 count = 0。​ • 第二次点击时,组件重渲染,count 是 1,setCount(2) 被调用。同时,一个新的 logMessage 函数被创建,它捕获了 count = 1。 以此类推。每个 setTimeout 的回调函数都持有自己被创建时 count 值的“快照”,因此打印出了符合我们直觉的结果。这种特性使得函数组件在处理异步逻辑时,行为更加可预测。​ ​ 生命周期 vs. 副作用:不同的关注点​ ​ • 类组件:以生命周期方法为中心,我们将逻辑分散到 componentDidMount(挂载时)、componentDidUpdate(更新时)、componentWillUnmount(卸载时)等方法中。这种方式的关注点是**时间**——组件在哪个生命阶段,就执行对应的逻辑。这常常导致相关联的逻辑被拆分在不同的方法里,不易维护。 • 函数组件:以 useEffect 为中心,我们将相关的逻辑组织在一起。useEffect 的思维模式是:**当某些数据(props 或 state)发生变化时,我需要同步执行某些副作用**。它的关注点是**数据同步**,而不是时间点。这使得我们可以将“数据获取和后续清理”、“事件监听和移除”等相关逻辑聚合在同一个 useEffect 块中,代码内聚性更强。 总结:我们应该如何选择?​ ​ 回到最初的问题,函数组件和类组件的本质区别在于:​ ​ 类组件是基于可变实例和生命周期的,其行为更像一个长期存活的对象;而函数组件是基于函数调用和闭包捕获的,其行为更像在特定时间点渲染出的快照。 ​ 在现代 React 开发中,我们应该如何选择?​ ​ 对于新项目或新功能,**优先并推荐使用函数组件和 Hooks**。这是 React 官方主推的范式,它带来了诸多优势:​ ​ 1. 代码更简洁:省去了 constructor、this 绑定等样板代码。​ 2. 逻辑复用更优雅:通过自定义 Hooks,可以轻松地抽离和复用带状态的逻辑,远比高阶组件(HOC)和 Render Props 模式更直观。​ 3. 心智负担更小:避免了 this 指向在 JavaScript 中的复杂性。​ 4. 行为更可预测:如前所述,闭包带来的值捕获特性让异步操作更符合直觉。 当然,这并不意味着类组件已经过时。在维护庞大的老旧项目时,我们依然会大量接触到类组件。因此,理解它的工作原理和心智模型,对于成为一名全面的 React 开发者来说,依然至关重要。​ ​ 最终,选择哪种方式不仅是技术选型,更是选择一种更符合现代 React 开发范式的心智模型。拥抱函数组件,就是拥抱一个更简洁、更函数式的未来。​ ​