☰
精读《Function VS Class 组件》:React 函数组件与类组件的 Capture Props 差异及 Hooks 替代实战
2026/10/2 2:05:47 网站建设 项目流程
  • 文档
  • 技术博客
  • 教程

【免费下载链接】weekly

前端精读周刊。帮你理解最前沿、实用的技术。

项目地址:https://gitcode.com/GitHub_Trending/we/weekly
点击查看免费下载

导读:本文基于前端精读周刊第 95 期《Function VS Class 组件》展开,围绕 React 中Function Component(函数组件)与 Class Component(类组件)的本质差异展开:先从 "Capture Props" 特性切入,说明两套写法在异步回调中读取 props 时行为为何不同;再系统性整理用 Hooks 替代shouldComponentUpdate、componentDidUpdate、forceUpdate等 Class 生命周期能力的实战方案;最后结合周刊中《React Hooks》《怎么用 React Hooks 造轮子》《Function Component 入门》《useRef 与 createRef 的区别》四篇配套精读,从渲染闭包、Ref 语义等源码级视角解释差异背后的原理。读完你可以准确预判两套写法在"延迟读取状态"场景下的输出差异,并掌握一整套 Hooks 化改造的落地套路。

为什么 Function Component 越来越重要

为什么要了解 Function 写法的组件呢?因为它正在变得越来越重要。

那么 React 中Function Component 与 Class Component 有何不同?原文《how-are-function-components-different-from-classes》带来了一个独特的视角:不讨论"谁更好",而是聚焦"特性差异"。

顺带一提,以后会用 Function Component 代替 Stateless Component 的说法。原因是:自从 Hooks 出现,函数式组件功能在不断丰富,函数式组件不再需要强调其无状态特性,因此叫 Function Component 更为恰当。

这个命名变化本身就是 Hooks 时代的一个缩影:函数组件从"只能做无状态展示"进化到"几乎无所不能",称谓自然也不再局限于 Stateless。

特性对比而非优劣对比

原文事先申明:并没有对 Function 与 Classes 进行优劣对比,而仅仅进行特性对比,所以不接受任何吐槽。

这两种写法没有好坏之分,性能差距也几乎可以忽略,而且 React 会长期支持这两种写法。

因此在阅读本文时,请把重点放在"理解差异、按场景选择"上,而不是陷入"谁淘汰谁"的争论。

Capture Props:两套写法行为差异

相同的逻辑,不同的写法

对比下面两段代码,它们描述的是同一个逻辑:点击按钮 3 秒后alert父级传入的用户名。

Class Component:

class ProfilePage extends React.Component { showMessage = () => { alert("Followed " + this.props.user); }; handleClick = () => { setTimeout(this.showMessage, 3000); }; render() { return <button onClick={this.handleClick}>Follow</button>; } }

Function Component:

function ProfilePage(props) { const showMessage = () => { alert("Followed " + props.user); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return <button onClick={handleClick}>Follow</button>; }

父级组件的调用方式:

<ProfilePageFunction user={this.state.user} /> <ProfilePageClass user={this.state.user} />

点击后的 3 秒里发生了什么

那么当点击按钮后的 3 秒内,父级修改了this.state.user,弹出的用户名是修改前的还是修改后的呢?

Class Component 展示的是修改后的值:3 秒后alert出来的是父级更新后的最新用户名(在线 Demo 可复现:先点击 Follow,再切换用户名,3 秒后弹出的是新用户名)。

Function Component 展示的是修改前的值:3 秒后alert出来的是点击那一刻闭包里的旧用户名,父级后续的更新对已经排队的回调毫无影响。

为什么 this.props 与 props 表现不同

这里有一个值得深思的疑问:React 文档中描述的props不是不可变(Immutable)数据吗?为啥在运行时还会发生变化呢?

原因在于,虽然props不可变,但this在 Class Component 中是可变的,因此this.props的调用会导致每次都访问最新的props。换句话说:不是 props 变了,而是this.props这个"指向"变了——旧的 props 对象找不到了。

而 Function Component 不存在this.props的语法,因此props总是不可变的:每次渲染都产生一份独立的props闭包,回调捕获的是它诞生那一刻的引用。

为了便于理解,补充一些代码注解:

Function Component:

function ProfilePage(props) { setTimeout(() => { // 就算父组件 reRender,这里拿到的 props 也是初始的 console.log(props); }, 3000); }

Class Component:

class ProfilePage extends React.Component { render() { setTimeout(() => { // 如果父组件 reRender,this.props 拿到的永远是最新的。 // 并不是 props 变了,而是 this.props 指向了新的 props,旧的 props 找不到了 console.log(this.props); }, 3000); } }

如果希望在 Class Component 捕获瞬时 Props,可以这样写:const props = this.props;——把当前渲染的 props 引用先固化下来。但这样的代码很蹩脚,而且容易漏写。所以,如果希望拿到稳定的props,使用 Function Component 是更好的选择。

Hooks 也具有 capture value 特性

Capture 行为并不只作用于 props,Hooks 的状态也一样。看下面的代码:

function MessageThread() { const [message, setMessage] = useState(""); const showMessage = () => { alert("You said: " + message); }; const handleSendClick = () => { setTimeout(showMessage, 3000); }; const handleMessageChange = e => { setMessage(e.target.value); }; return ( <> <input value={message} onChange={handleMessageChange} /> <button onClick={handleSendClick}>Send</button> </> ); }

在点击Send按钮后,再次修改输入框的值,3 秒后的输出依然是点击前输入框的值。这说明 Hooks 同样具有 capture value 的特性——showMessage闭包捕获的是它创建时所在渲染的message。

用 useRef 规避捕获

利用useRef可以规避 capture value 特性:

function MessageThread() { const latestMessage = useRef(""); const showMessage = () => { alert("You said: " + latestMessage.current); }; const handleSendClick = () => { setTimeout(showMessage, 3000); }; const handleMessageChange = e => { latestMessage.current = e.target.value; }; }

只要将赋值与取值的对象变成useRef,而不是useState,就可以躲过 capture value 特性,在 3 秒后得到最新的值。

为什么useRef能做到?因为它返回的是一个跨渲染共享的可变容器:ref.current的读写不依赖任何渲染闭包,无论在哪次渲染里读写,访问的都是同一份内存。这正是周刊第 141 篇《useRef 与 createRef 的区别》强调的语义——useRef的值在 Function Component 的所有渲染周期之间共享、引用不变。

这说明了利用 Function Component + Hooks 可以实现 Class Component 做不到的 capture props、capture value,而且 React 官方也推荐新的代码使用 Hooks 编写。

原理纵深:渲染闭包与 this 的可变性

为什么 Function 组件天然具备捕获能力?周刊第 104 篇《精读《Function Component 入门》》里有一个更直观的计数器实验,可以加深理解。

104 篇的计数器实验:0 1 2 vs 3 3 3

同样是"点击后自增、3 秒后再打印",Function 写法三秒内连点三次,打印结果是:

0 1 2

而 Class 写法(this.setState+this.state.count)三秒内连点三次,打印结果是:

3 3 3

原因拆解如下:

  • Class 组件:state本身是 Immutable 的,setState后生成全新引用;但通过this.state读取,每次代码执行都会拿到最新的 state 引用,所以是3 3 3。
  • Function 组件:useState产生的数据同样是 Immutable 的,但由于读取没有经过this.,每次setTimeout都读取了当时渲染闭包环境的数据——最新值确实跟着最新渲染更新了,但旧渲染闭包里的状态依然是旧值。

可以这样在脑中模拟:每一次渲染都是一次独立的函数调用、一份独立的闭包。三次点击对应三次"捕获了 count 旧值"的闭包,count在三次渲染中的值分别是0 1 2,所以无论setTimeout延时多久,打印永远是0 1 2。

闭包与可变引用的本质区别

把两个实验放在一起看,本质就清晰了:

维度Class ComponentFunction Component
状态读取方式this.state/this.props(可变 this)闭包变量(不可变捕获)
异步回调中读到的是读取时刻的最新值回调创建时刻的值
对应能力永远拿到最新值(无需额外处理)capture value(天然快照)
想要相反行为需const props = this.props固化需useRef容器绕过捕获

两种行为没有对错,而是"指向最新"与"捕获快照"两种心智模型的差异。理解了这一点,就能准确预判异步场景下两套写法的输出。

精读:Function Component 常见问题与生命周期替代

原文之所以将 Function Component 与 Class Component 相提并论,几乎都要归功于 Hooks API 的出现:有了 Hooks,Function Component 的能力才得以向 Class Component 看齐。周刊第 79 篇《精读《React Hooks》》 与第 80 篇《怎么用 React Hooks 造轮子》 分别介绍了 Hooks 的基本认知与实战用法,可配合阅读。

不过要注意当时的结论:虽然 Hook 已经发布了稳定版本,但周边生态跟进还需要时间(比如useRouter)、最佳实践整理还需要时间,因此不建议重构老代码。

为了更好的使用 Function Component,建议时常与 Class Component 的功能做对比,方便理解和记忆。下面整理一些常见的 Function Component 问题:

怎么替代 shouldComponentUpdate

说实话,Function Component 替代shouldComponentUpdate的方案并没有 Class Component 优雅,代码是这样的:

const Button = React.memo(props => { // your component });

或者在父级就直接生成一个自带memo的子元素:

function Parent({ a, b }) { // Only re-rendered if `a` changes: const child1 = useMemo(() => <Child1 a={a} />, [a]); // Only re-rendered if `b` changes: const child2 = useMemo(() => <Child2 b={b} />, [b]); return ( <> {child1} {child2} </> ); }

相比之下,Class Component 的写法通常是:

class Button extends React.PureComponent {}

这样就自带了shallowEqual的shouldComponentUpdate。

补充一点原理:React.memo是函数组件版的"PureComponent",会在自身重渲染时对每个 props 项做浅对比,引用没变就不触发重渲染;而useMemo的优势是更细粒度的优化——组件整体可能依赖A、B两个 props,但某段渲染只用到了B,用memo时A变化仍会重渲染,用useMemo则不会。更深入的分析可见周刊第 104 篇 的"用 memo 做 PureRender / 用 useMemo 做局部 PureRender"两节。

怎么替代 componentDidUpdate

由于useEffect每次 Render 都会执行,因此需要模拟一个useUpdate函数:

const mounting = useRef(true); useEffect(() => { if (mounting.current) { mounting.current = false; } else { fn(); } });

原理很简单:用mounting这个 Ref 标记首次挂载,首次渲染只置为false不执行fn;此后每次渲染(更新)都执行fn。周刊第 80 篇 的"模拟生命周期"一节给出了完整实现:componentDidMount等价于useEffect(() => void fn(), []),componentWillUnmount等价于useEffect(() => fn, []),componentDidUpdate等价于"mounting flag + 无依赖限制",三者可以组成一套生命周期全家桶。

怎么替代 forceUpdate

React 官方文档提供了一种方案:

const [ignored, forceUpdate] = useReducer(x => x + 1, 0); function handleClick() { forceUpdate(); }

每次执行dispatch时,只要state变化就会触发组件更新。当然useState也同样可以模拟:

const useUpdate = () => useState(0)[1];

我们知道useState下标为 1 的项是用来更新数据的,而且就算数据没有变化,调用了也会刷新组件,所以我们可以把返回一个没有修改数值的setValue,这样它的功能就仅剩下刷新组件了。第 80 篇 里还有更严谨的写法:

const useUpdate = () => { const [, setState] = useState(0); return () => setState(cnt => cnt + 1); };

即用函数式更新保证每次 +1,彻底规避"数据没变不触发渲染"的边界情况。

注意:getSnapshotBeforeUpdate、getDerivedStateFromError、componentDidCatch目前 Hooks 是无法模拟的,错误边界仍需 Class 组件。

state 拆分过多

useState目前的一种实践,是将变量名打平,而非像 Class Component 一样写在一个 State 对象里:

class ClassComponent extends React.PureComponent { state = { left: 0, top: 0, width: 100, height: 100 }; } // VS function FunctionComponent { const [left,setLeft] = useState(0) const [top,setTop] = useState(0) const [width,setWidth] = useState(100) const [height,setHeight] = useState(100) }

实际上在 Function Component 中也可以聚合管理 State:

function FunctionComponent() { const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 }); }

只是更新的时候,不再会自动 merge,而需要使用...state语法:

setState(state => ({ ...state, left: e.pageX, top: e.pageY }));

可以看到,更少的黑魔法,更可预期的结果:Class 的setState是浅合并,而useState是整体替换,后者把"合并"这一隐含行为显式化了。

获取上一个 props

虽然不怎么常用,但是毕竟 Class Component 可以通过componentWillReceiveProps拿到previousProps与nextProps,对于 Function Component,最好通过自定义 Hooks 方式拿到上一个状态:

function Counter() { const [count, setCount] = useState(0); const prevCount = usePrevious(count); return ( <h1> Now: {count}, before: {prevCount} </h1> ); } function usePrevious(value) { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }

通过useEffect在组件渲染完毕后再执行的特性,再利用useRef的可变特性,让usePrevious的返回值是"上一次" Render 时的。周刊第 141 篇《useRef 与 createRef 的区别》 指出,这一模式正是官方利用 Ref 封装previousProps的标准做法:由于useEffect在 Render 完毕后才执行,ref的值在当前 Render 中永远是上一次 Render 时写入的,因此usePrevious(props)拿到的就是上一次 props。这也再次体现了ref可以将值"在各个不同 Render 闭包中传递"的特性。

可见,合理运用useEffect、useRef,可以做许多事情,而且封装成 CustomHook 后使用起来仍然很方便。

当时预测:未来usePrevious可能成为官方 Hooks 之一。

性能注意事项:useState 初始值懒计算

useState函数的参数虽然是初始值,但由于整个函数都是 Render,因此每次初始化都会被调用,如果初始值计算非常消耗时间,建议使用函数传入,这样只会执行一次:

// Bad function FunctionComponent(props) { const [rows, setRows] = useState(createRows(props.count)); } // Good function FunctionComponent(props) { const [rows, setRows] = useState(() => createRows(props.count)); }

createRows(props.count)这种写法会在每一次渲染时都执行一次createRows(尽管结果只被首次渲染使用),而() => createRows(props.count)惰性初始化的写法只在首次渲染执行一次。

注意:useRef不支持这种特性,需要写一些冗余的判断是否进行过初始化(懒初始化 Ref 的官方推荐写法是:在函数内判断ref.current === null再创建昂贵对象,如IntersectionObserver,这种初始化最多执行一次、仅用于赋值,是被允许的副作用,详见第 141 篇)。

掌握了这些,Function Component 使用起来与 Class Component 就几乎没有差别了!

仓库配套精读的源码级佐证

本仓库是以精读文章形式组织的技术周刊,下面四篇与本主题强相关的文章,可以作为深入理解上述方案底层原理的延伸阅读:

  • 79.精读《React Hooks》:论证 Hooks 本质是"打平的 renderProps"——useState返回的[值, 赋值函数]等价于一次依赖注入,多个状态平铺而不嵌套;并给出了用useState自建useReducer模拟 Redux、用useEffect收敛生命周期碎片(如 G2 图表初始化与销毁)的示范。
  • 80.精读《怎么用 React Hooks 造轮子》:给出useMount、useUnmount、useUpdate、useIsMounted、forceUpdate 等一整套生命周期模拟实现,以及useInputValue(组件辅助)、useAsync(发请求)、useFormState(填表单)等轮子,印证了本文所有替代方案的可行性。
  • 104.精读《Function Component 入门》:用"三秒内连点三次"的实验逐渲染展开闭包捕获过程,并深入setInterval场景,引出"永远对 useEffect 依赖诚实"、useCallback抽离函数、useEventCallback规避函数重复实例化、React.memo/useMemo渲染优化等进阶主题。
  • 141.精读《useRef 与 createRef 的区别》:解释useRef跨渲染共享引用、createRef在函数组件中会随渲染反复初始化的差异,以及"Render 阶段禁止写副作用、Commit 阶段或回调中修改 Ref"的规范。

这些文章共同构成了"从 Function vs Class 差异,到 Hooks 化实战,再到底层渲染原理"的完整学习路径。

总结:选择函数式的好与坏

Function Component 功能已经可以与 Class Component 媲美了,但目前最佳实践比较零散,官方文档推荐的一些解决思路甚至不比社区第三方库的更好,可以预料到,Class Component 的功能会被五花八门的实现出来,那些没有被收纳进官方的 Hooks 乍看上去可能会眼花缭乱。

总之选择了 Function Component 就同时选择了函数式的好与坏:

  • 好处是功能强大:几乎可以模拟出任何想要的功能,capture props、capture value 是 Class 组件难以自然获得的特性,配合useRef又可以随时"打开"通往最新值的大门。
  • 坏处是由于可以灵活组合:如果自定义 Hooks 命名和实现不够标准,函数与函数之间对接的沟通成本会更大。

结合仓库中第 79、80、104、141 篇的配套解读,推荐的实践路径是:新代码优先考虑 Function Component + Hooks,用React.memo/useMemo处理渲染优化,用自定义 Hooks 封装可复用的状态逻辑,用useRef处理"需要跨闭包读取最新值"的少数场景,并始终对useEffect的依赖保持诚实——这样既能享受函数式的表达力,又能把"灵活"带来的失控风险降到最低。

  • 文档
  • 技术博客
  • 教程

【免费下载链接】weekly

前端精读周刊。帮你理解最前沿、实用的技术。

项目地址:https://gitcode.com/GitHub_Trending/we/weekly
点击查看免费下载
上一篇:MetaboAnalystR 4.0:LC-MS代谢组学全流程分析框架深度技术解析
下一篇:BilibiliCacheVideoMerge:如何将B站缓存碎片一键合并为完整MP4视频?

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询