- 文档
- 技术博客
- 教程
【免费下载链接】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 Component | Function 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
前端精读周刊。帮你理解最前沿、实用的技术。
相关推荐
reactstrap函数式组件:Hooks完全替代class组件
reactstrap函数式组件:Hooks完全替代class组件 你还在为class组件的繁琐生命周期管理而烦恼吗?还在为this绑定问题调试到深夜吗?本文将带
UI组件YTPro第三方库集成指南:使用哪些开源项目提升YouTube体验
YTPro第三方库集成指南:使用哪些开源项目提升YouTube体验 YTPro是一款功能强大的YouTube客户端应用,通过精心选择的第三方库集成,为用户提供了
react-redux-starter-kit中的React Hooks迁移指南:从class组件到函数组件
react redux starter kit中的React Hooks迁移指南:从class组件到函数组件 你还在为项目中大量class组件难以维护而烦恼吗?
前端示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考