精读《Function VS Class 组件》:React 函数组件与类组件的 Capture Props 差异及 Hooks 替代实战
文档技术博客教程【免费下载链接】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(() { // 如果父组件 reRenderthis.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 行为并不只作用于 propsHooks 的状态也一样。看下面的代码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.setStatethis.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 的出现有了 HooksFunction 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 无依赖限制三者可以组成一套生命周期全家桶。怎么替代 forceUpdateReact 官方文档提供了一种方案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 中也可以聚合管理 Statefunction 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点击查看免费下载相关推荐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组件难以维护而烦恼吗前端示例工程上一篇MetaboAnalystR 4.0LC-MS代谢组学全流程分析框架深度技术解析下一篇BilibiliCacheVideoMerge如何将B站缓存碎片一键合并为完整MP4视频创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联
返回资讯列表 →