React组件创建方式全解析:函数组件、Hooks、HOC与性能优化

发布时间:2026/10/9 23:29:51

React组件创建方式全解析:函数组件、Hooks、HOC与性能优化 1. 先聊清楚组件在 React 里到底是一种什么“东西”1.1 组件的本质是“函数”还是“描述”React 开发者每天的工作几乎都绕不开组件页面是组件按钮是组件一个表单是组件甚至一个 SVG 图标也是一个组件。很多人会用“组件就是页面的一部分”这种话来解释但真正写起来会发现这个说法很容易把人带偏。React 组件并不是一块切割好的 UI 区域它更像是一个“可以返回界面描述的函数”。同一个组件可以渲染在列表里、弹窗里、路由页面里只要在 JSX 里Component /React 就会执行这个函数拿到它返回的虚拟元素再交给渲染引擎去绘制真实 DOM。为什么强调“函数”而不是“模板”因为在 React 的运行时里函数组件本身就是一种普通函数只不过它的返回值必须是可渲染的描述对象。class 组件从本质上看也是一样的只是换了一副壳你用 class 的方式定义了一个组件类React 会帮你实例化它再调用 render 方法拿到描述。理解了这一层后面再聊函数组件、类组件、高阶组件、Render Props就都不容易混乱了。1.2 为什么要费劲区分“创建方式”很多人会觉得“能跑就行纠结函数组件还是 class 组件有什么意义”。其实这个问题的答案直接关系到项目的维护成本、性能优化空间、协作效率甚至面试时的表达深度。React 组件创建方式经历过几次明显的变迁早期只有 class 写法后来函数组件只能做纯展示再后来 React 16.8 引入了 Hooks函数组件一下子拥有了状态、生命周期副作用、缓存等能力。到了现在新项目几乎默认函数组件 Hooksclass 组件渐渐退到旧项目维护场景里。但这并不意味着 class 写法没有价值很多公司存量系统里全是 class 组件遇到这类老页面你必须得看得懂、能改得动。真正的高手不是只背“新的更好”这个结论而是能说清楚每种方式的适用边界。本文就围绕“创建组件”这个话题把函数式、类式、高阶组件、Render Props、自定义 Hook、memo 优化、懒加载这些常见的创建姿势都过一遍。重点不是罗列语法而是讲清楚每种方式解决什么问题、坑在哪里、什么场景选什么更合理。2. 传统且依然能打的两种方式函数组件与类组件2.1 用函数造组件从无状态到 Hooks函数组件最原始的形态特别简单function Button({ label }) { return button{label}/button; }这本质上就是一个纯函数输入 props返回 JSX。在没有 Hooks 的年代函数组件只能做这种“拿着 props 渲染”的事情所以也常常被叫做“无状态组件”。当时如果某个组件需要内部状态比如打开一个下拉菜单、记录输入框的值你就只能把它改成 class 组件。React 16.8 之后情况完全变了。useState 给函数组件带来了内部状态useEffect 接管了生命周期副作用useContext、useRef、useReducer 这些能力让函数组件几乎能覆盖所有场景。同样是上面的按钮想在点击时统计次数function CounterButton({ initialCount 0 }) { const [count, setCount] useState(initialCount); return ( button onClick{() setCount(count 1)} 点击了 {count} 次 /button ); }我实际写下来的感受是函数组件比 class 组件“更轻”这种轻体现在心智负担上。没有 this 指向问题没有生命周期拆分的强迫症状态和副作用可以按业务逻辑自由组合。比如一个订阅倒计时的效果在 class 里要往 componentDidMount 和 componentWillUnmount 里分别塞代码数据还不容易共享在函数组件里一个 useEffect 就能把“订阅、更新、清理”收拢在一起useEffect(() { const timer setInterval(() setNow(new Date()), 1000); return () clearInterval(timer); }, []);这个 return 函数就是清理函数React 会在组件卸载或依赖变化前执行它相当于帮你把 componentWillUnmount 的事一并办了。这种收敛是函数组件最舒服的地方。2.2 用 Class 造组件生命周期和 this 的代价class 组件长这样class Button extends React.Component { constructor(props) { super(props); this.state { count: props.initialCount || 0 }; } handleClick () { this.setState((prev) ({ count: prev.count 1 })); }; render() { return button onClick{this.handleClick}点击了 {this.state.count} 次/button; } }class 组件有几个标志性特点必须继承 React.Component、必须有 render 方法、状态写到 this.state、更新状态用 this.setState。这也引出了一系列问题setState 是异步批量的连续调用需要传函数式 updaterthis 的绑定容易踩坑不用箭头函数就得 bind生命周期方法名称多且容易记混比如 componentDidMount、componentDidUpdate、componentWillUnmount还有一个官方已经不建议使用的 componentWillReceiveProps。那 class 组件还有没有存在价值有而且不少存量项目里有大量这样的代码。理解 class 组件不能只看语法还要能看到当时的设计动机它希望通过生命周期方法把“挂载、更新、卸载”这些时机暴露给开发者。比如在 componentDidMount 里请求接口在 componentDidUpdate 里对比 props 变化这在 Hooks 问世前的确是唯一自然的写法。如果你平时主要写函数组件但仍然需要维护 class 组件我的建议是不要急着把所有 class 全都重写成函数式除非你真的在改 bug 或做重构。class 组件和函数组件可以共存React 自身并不在意你用哪种方式创建。真正需要注意的是把生命周期方法干什么事理清楚避免在特殊生命周期里做重复请求这种低级错误。2.3 两者对比与转换心得维度函数组件class 组件定义方式普通函数继承 React.Component状态定义useState / useReducerthis.state状态更新state 更新函数this.setState副作用useEffectcomponentDidMount 等生命周期this 指向不涉及需要箭头函数或 bind心智模型“函数返回界面”“实例 生命周期”面向新项目推荐不推荐存量项目维护可用常见也需要掌握如果你需要把 class 组件改成函数组件核心思路是state 拆成 useStateclass 里的多个 this.setState 合并成一次对象更新componentDidMount 和 componentDidUpdate 组合成一个或多个 useEffect注意依赖数组不要无脑不写否则副作用会持续触发componentWillUnmount 的清理代码放到 useEffect 的 return 函数里。我在实际重构中踩过的最大坑是多云场景下请求竞态class 里可以用 mounted 标记位判断函数组件里则要在 useEffect 清理函数里取消请求或标记无效。3. 给组件加逻辑HOC、Render Props 和自定义 Hook3.1 HOC包装函数再返回一个组件高阶组件Higher-Order Component不是一个 API而是一种基于 React 组件组合能力的模式。它的思想很直接你写一个函数接收一个组件最终返回一个增强后的新组件。React 社区早期特别流行这种玩法典型应用是权限校验、日志埋点、数据请求注入。function withLoading(WrappedComponent) { return function LoadingWrapper(props) { if (!props.loaded) { return div加载中.../div; } return WrappedComponent {...props} /; }; }用法就是把原有组件包一层const UserListWithLoading withLoading(UserList);这里的 withLoading 就是 HOC。它的优点是复用逻辑时“包装”的意图很清晰多个 HOC 可以像套娃一样组合。缺点也很明显props 来源不透明、命名冲突风险、多层嵌套后调试起来很痛苦。我在旧项目里经常看到这种五层以上 HOC 嵌套的组件props 有时候根本不知道是哪一层传进来的排查问题全靠 console.log。所以后来思考总结时我倾向于把 HOC 当“组合工具”而不是“逻辑封装庙庙主”。3.2 Render Props把渲染逻辑交给调用方Render Props 是另一种创建组件的思路组件本身不决定如何渲染结果而是把渲染函数作为 props 传进来让别人决定。function MouseTracker({ render }) { const [position, setPosition] useState({ x: 0, y: 0 }); // 监听鼠标移动... return render(position); } // 使用 MouseTracker render{({ x, y }) ( div鼠标位置{x}, {y}/div )} /这种写法把控制权翻转了状态逻辑留在组件内部显示逻辑由调用方通过函数自由定制。它的好处是渲染自由度高、没有额外的层级包裹坏处是函数嵌套过深之后可读性下降而且 render prop 函数的 props 传递不够直观容易写出一大段“地狱回调”的形状。我在封装下拉选中项的自定义展示内容时用过这种方式场景很合适但后来用了 Hook 之后基本就不再写了因为 Hook 能更直接地拿到状态且不产生层级。3.3 自定义 Hook函数组件时代的最佳逻辑复用方式自定义 Hook 本质上就是“把可复用的逻辑封装成一个函数”函数中以 use 开头内部使用 React 的 Hooks。它不像 HOC 那样包装组件也不像 Render Props 那样传函数而是直接把状态和操作暴露给外部使用起来就像调用一个普通函数。还是刚才的鼠标位置逻辑function useMousePosition() { const [position, setPosition] useState({ x: 0, y: 0 }); useEffect(() { const handler (e) setPosition({ x: e.clientX, y: e.clientY }); window.addEventListener(mousemove, handler); return () window.removeEventListener(mousemove, handler); }, []); return position; } function MouseTracker() { const { x, y } useMousePosition(); return div鼠标位置{x}, {y}/div; }这段代码的可读性比 Render Props 好太多因为状态的源和消费处都在同一个组件视角里没有嵌套。自定义 Hook 还有一个特别实用的场景组合多个组件共享的业务逻辑。比如多个表单页都需要拉取省市区数据那你就可以做 useRegionData Hook返回数据、加载状态和错误。我的经验是写自定义 Hook 时不要把“返回完整 JSX”当作目标它应该返回数据和操作渲染交给组件自己这样复用价值最大。4. 面向性能的组件创建姿势memo、PureComponent 与懒加载4.1 React.memo 不是拿来即用的银弹函数组件没有内置的 shouldComponentUpdate不像 class 组件可以主动告诉 React“这个组件 props 没变化就别渲染”。为了解决这个问题React 提供了 React.memo。它本质上是“记忆化组件”对 props 做浅比较如果 props 没变化就跳过组件重新渲染。const ExpensiveChart React.memo(function ExpensiveChart({ data }) { return Chart data{data} /; });但这个“浅比较”非常有迷惑性。比如父组件执行了如下代码function Parent() { const [count, setCount] useState(0); return ( ExpensiveChart data{{ type: bar }} / ); }每次 Parent 重新渲染data 变量都会创建出一个新对象引用地址变了React.memo 浅比较发现 props.data ! 上次的 props.data于是还是会重新渲染。也就是说memo 只在 props 引用稳定的前提下发挥作用。这就牵扯到 useMemo、useCallback 的使用如果传的是函数要用 useCallback传的是对象或数组要用 useMemo。我踩过很多次“加了 memo 没效果”的坑最后发现是忘了稳定 props 引用。从这个角度说memo 不是为了“整个组件不重渲染”准备的而是要求你把数据流设计得足够稳定它才能锦上添花。4.2 懒加载组件lazy 和 Suspense 的实践懒加载并不是一种完全独立的组件创建方式它是在组件定义的基础上改变组件的“加载时机”。React.lazy 配合 Suspense 可以让一个按需加载的组件的代码在真正渲染时才下载这样首屏体积能明显减少。const UserDashboard React.lazy(() import(./UserDashboard)); function App() { return ( React.Suspense fallback{div加载中.../div} UserDashboard / /React.Suspense ); }这里有几个细节值得注意。第一import函数的返回值必须是一个 Promise并且这个 Promise resolve 的模块默认导出必须是组件React.lazy 目前只认 default export。如果你想导出的不是 default可以这样过渡const UserDashboard React.lazy(() import(./components).then((module) ({ default: module.UserDashboard })) );第二Suspense 必须包裹在 lazy 组件外围它才能捕获组件内部的加载 Promise。第三路由懒加载也要结合 BrowserRouter 等路由组件使用实现按路由拆分代码块。我在中大型项目里常用这种方案来拆分大模块不过可别过度拆分否则一个页面有成百上千个 chunk 会在网络请求上消耗更多时间反而得不偿失。一般粒度在页面级或者复杂组件级比较合理。4.3 一个性能优化组件的小经验组件性能优化不是从 React.memo 开始而是从测量开始的。React DevTools 的 Profiler 面板可以记录组件的渲染耗时和触发原因我一般先看哪些组件被意料之外地反复渲染再针对性处理。常见策略里值得注意的三个方向一是避免内联函数和对象在渲染时反复创建可以把公共方法用 useCallback 包起来二是列表项组件保持唯一且稳定的 key三是尽量把“变化的部分”拆成边界清晰的子组件让组件与脏数据隔离。这些方式看似简单但我见过很多团队直接给所有组件套 React.memo不仅没起到效果还让代码更难读。正确做法是先看实际数据流再决定优化手段。5. 组件封装与代码组织的实操建议5.1 组件创建在文件里的位置和导出方式很多新手写组件喜欢一个文件里放好几个组件靠 export default 和 export function 混着导出。这种方式能跑但维护起来很乱。我的经验是一个组件一个文件用 named export 导出组件本身同时导出与它强相关但需要单独测试的小工具函数。例如export function Pagination({ page, total }) { ... } export function buildPageNumbers(page, total) { ... }这样既有默认的组件入口也能单独对 buildPageNumbers 写单测。如果你要支持 library 模式会在 index.js 里收口导出export { Pagination } from ./Pagination; export { default as PaginationItem } from ./PaginationItem;组件文件放置的目录结构也值得一提。小型项目组件少按功能分开就行大型项目我推荐“一个目录一个业务组件”的结构components/ Pagination/ Pagination.tsx Pagination.test.tsx index.ts styles.module.css这样组件、样式、测试都聚在一起迁移时直接移动目录不会翻箱倒柜。5.2 复合组件模式与 forwardRef创建组件不只有“写一个 function”这一种姿势还有一种很适合表单、下拉、Tab 这类业务场景的“复合组件”模式外层组件负责状态内部子组件通过 props 或 Context 共享数据。比如一个最简单的 Tabsconst TabsContext createContext({ active: 0, setActive: () {} }); function Tabs({ defaultActive 0, children }) { const [active, setActive] useState(defaultActive); return ( TabsContext.Provider value{{ active, setActive }} {children} /TabsContext.Provider ); } Tabs.Panel function Panel({ index, children }) { const { active } useContext(TabsContext); if (active ! index) return null; return div{children}/div; };使用Tabs defaultActive{0} Tabs.Panel index{0}面板一/Tabs.Panel Tabs.Panel index{1}面板二/Tabs.Panel /Tabs这种组件创建的思路把 API 设计得很像“子标签”用户不用单独 connect 状态体验很好。它背后的核心知识是 Context createContext属于函数组件时代重要的组件编排方式。另外当你想把组件内部的真实 DOM 节点暴露给父级时可以用 React.forwardRef。比如你封装了一个输入框希望父组件能直接调用 focus 方法const MyInput React.forwardRef(function MyInput(props, ref) { return input ref{ref} {...props} /; });这不是一种全新组件而是一个“转发式”的创建技巧库作者导出组件时很常用。5.3 常见的坑在 render 中定义组件、函数组件的 name很多人会在 JSX 里顺手写一个内联函数组件function BadComponent() { const Child () divhello/div; return Child /; }这非常致命。因为每次 BadComponent 重新渲染时Child 都是一个全新的函数引用React 会认为组件类型变了于是会卸载旧组件、挂载新组件导致状态丢失、性能下降。正确做法是把 Child 组件提到外层或者单独文件。另一个容易被忽略的问题是函数组件的 displayName。HOC 包了一层之后React DevTools 默认显示的是包装函数名难排查。你可以给返回的函数设置 displayName或者直接用 React DevTools 的“设置 - 记录组件原因”辅助定位。我的习惯是写高阶组件时手动命一下名withLoading.displayName withLoading(${getDisplayName(WrappedComponent)});这个小细节能帮你在调试大项目时省掉不少时间。6. 创建组件时最容易被问到的那几个问题6.1 生命周期函数和 Hooks 怎么对应class 组件时代大家常问生命周期函数组件时代对应关系也要心里有数。componentDidMount 大致对应 useEffect(() { ... }, []) 的空依赖写法componentDidUpdate 大致对应 useEffect(() { ... }) 不写依赖数组的写法但要注意这种写法会每次渲染都执行最好还是把依赖列出来componentWillUnmount 对应 useEffect 里 return 的清理函数。这块最常见的问题是有人以为 useEffect 传了空数组就能模拟 componentWillUnmount然后在里面写“移除监听”的代码结果发现组件卸载不掉清理函数也没执行。这是因为清理函数是伴随 effect 的只有依赖变化或卸载时才会执行。如果你要在卸载时清理就得在 useEffect 里先注册再 return 清理不能把 return 直接当成独立的监听器。6.2 组件通信方式如何体现在“创建”过程中组件创建方式直接影响数据怎么流进来。父传子通过 props子传父通过回调函数跨层级用 Context复杂状态机用 reducer 加 dispatch。创建组件之前先把通信方式想清楚组件接口就会稳定很多。我有一次封装一个全局弹窗组件一开始把所有状态都放在组件内部导致外部难以触发。后来改成受控模式把 open、onClose 全通过 props 暴露组件立刻好用起来。这其实说明一个道理组件创建方式不是孤立的语法选择它决定了边界在哪、状态归谁、接口长什么样。你写的每一对 useState 和 props都在为未来的维护者做约定。7. 最后分享一个我自己常用的组件设计小技巧我最近写业务组件越来越喜欢“函数组件 自定义 Hook 复合组件”的组合函数组件负责渲染和搭布局自定义 Hook 负责业务数据流复合组件模式负责把 API 变得贴近语义。比如一个年份月份选择器我先是把年月的状态逻辑放进 useYearMonth然后组件里用这个 Hook 拿到值和修改函数再把面板拆成子组件挂到父组件上。这样写下来逻辑单独能测页面上调用处非常顺眼。有一个容易忽视的细节是组件内部只要出现“完整业务规则”就应该把这些规则抽离成纯函数或独立模块让组件只粘合 UI 与数据。一旦规则改变你不必在组件里翻代码。这个习惯让我在多次需求变动中少踩了很多坑。React 创建组件的方式还有很多边角料比如 SSR 环境下的组件、微前端里的子应用组件、画布类组件等等但核心思路都是先确定边界、选对范式、再写实现。你现在可以从手边一个只有一个文件的小项目开始试着把里面的组件改成函数式 自定义 Hook 的风格跑一轮测试看看可读性和维护性有没有变化。等有一天你觉得“组件怎么创建”这个问题已经不再重要那就说明你已经慢慢拥有了自己稳定的组件设计观。
延伸阅读

更多相关文章

2026/10/9 23:24:51

基于YOLO的寄生虫虫卵检测:数据集解析与训练实践

1. 先聊聊:寄生虫虫卵检测为什么要上 YOLO检验科显微镜检查这件事,老检验人应该都有体会:一张粪便涂片看下来,眼睛酸胀是常态,碰上形态相近的几种虫卵,还得反复调焦、换视野、对照图谱。寄生虫虫卵的判定&a…

2026/10/9 23:24:51

VFP图书借阅管理系统:数据库课程设计完整源码与避坑指南

简介:《学校图书借阅管理系统》数据库系统设计课程设计报告,面向计算机、信息管理类专业学生及需要完成数据库课程设计的开发者。报告以学校图书馆的借阅业务为背景,涵盖需求分析、数据字典、数据流图、概要设计、E-R图、详细代码实现及运行结…

2026/10/9 23:24:51

3000 免费积分冲上热搜:MiniMax 全系模型白嫖潮又来了

3000 免费积分冲上热搜:MiniMax 全系模型白嫖潮又来了 【免费下载链接】Minimax-h3_Singularity 项目地址: https://ai.gitcode.com/hf_mirrors/WarmBloodAban/Minimax-h3_Singularity 下载一个 App 就给 3000 积分、3 次旗舰视频模型免费生成机会&#xff…

2026/10/10 3:45:11

顽固木马杀不死?内核级专杀工具与常规杀软的区别及实战

正在处理一份上周的文件,电脑忽然像被什么东西按住一样卡住不动,安全软件图标打不开,任务管理器里冒出几个乱码名字的进程。最气人的是,等我用常规杀毒软件全盘扫一遍,它提示“未发现威胁”,可重启之后症状…

2026/10/10 3:45:11

恶性木马专杀实战:内核级查杀与顽固病毒清理指南

电脑感染恶性木马,和普通病毒骚扰完全是两种体验。普通木马最多是弹窗、改首页、后台偷偷占资源,真正麻烦的是那种系统被恶意驱动接管、杀毒软件打不开、进程在任务管理器里看不到、重启之后病毒又原地复活的顽固感染。这种场景下,“火绒恶性…

2026/10/10 3:45:11

文本挖掘实战指南:从非结构化数据到决策信号

每次拿到一堆客服工单、用户反馈、社交媒体的评论,我都觉得头疼。这些文本数据又多又乱,但又确确实实藏着用户最真实的声音——满意度、痛点、产品缺陷、竞品动向,全在里面。问题是,它们都是非结构化数据,没法直接塞进…

2026/10/10 3:45:11

Win11 TPM2.0不可用?华硕主板fTPM开启全指南

1. 为什么Win11升级卡在“TPM 2.0不可用”?——不是硬件不支持,而是BIOS里藏着开关你点开Windows更新,看到那行加粗的红色提示:“此电脑无法运行Windows 11。缺少必需的安全功能:可信平台模块(TPM&#xff…

2026/10/10 3:45:11

少即是多:用减法重构程序员、PM与项目经理的生活系统

上周某晚,我在公司楼下等电梯,看到群里有人抛出一个问题:程序员、产品经理、项目经理,谁最不可能准时下班?评论区瞬间吵成一团,有人自嘲“三班倒”,有人说“谁有孩子谁先走”,还有人…

2026/10/10 3:40:10

RabbitMQ消费端可靠性实战:限流、超时与死信队列全解析

凌晨两点十七分,我手机上的告警通道开始连续发声。监控面板显示,order_process队列的 Ready 消息数在十分钟内从 200 冲到了 5000,而消费者进程明明还活着,日志里却在疯狂刷同一条消息的消费失败栈——又是那台订单处理服务。做 J…

2026/10/8 10:03:18

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/9 20:15:56

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/8 6:05:44

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/10 0:04:53

从逻辑门到计算机:数字电路核心原理与全加器搭建实战

如果你拆过一台旧电脑的主板,盯着那些黑乎乎的小芯片看上一会儿,可能会冒出同一个疑问:这堆引脚密集的元件,到底是怎么“变”出那么复杂的应用的?答案并不在某个神秘的部件里,而是在所有芯片内部都在反复使…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑