拓冰建站拓冰建站
首页 / 资讯中心 / 正文

前端框架为何弃用Class?函数组件与Hooks的底层逻辑

这两年带前端团队招人、做技术评审我反复被问到一个问题为什么现在写React、Vue大家几乎不怎么写Class了别说新项目就是看开源社区类组件的占比也在肉眼可见地萎缩。我自己的技术栈从早期jQuery时代的面向对象封装到React的class组件再到现在函数组件加Hooks一路走过来说实话这个变化不是某个框架拍脑袋决定的而是前端开发模型一次底层逻辑的切换。这篇就聊聊我观察到的趋势以及藏在“类用得少了”这个现象背后的几个真正原因。如果你正在学前端或者处于“类的组件写法还能不能学”的纠结里这篇文章应该适合你。我会把Class在前端框架里遇到的痛点、函数组件加Hooks为什么能补上这些短板、以及类到底是不是真的“凉了”一次性讲透。1. 这波趋势的本质从“对象加生命周期”到“函数加状态同步”1.1 类组件当年是怎么成为主流的在React 16.8之前类组件基本是复杂组件的唯一选择。函数组件只能做纯展示一旦涉及到state、生命周期你就必须写一个class。Vue 2那边虽然官方推荐对象字面量写法但在TypeScript加持下很多人也会用vue-class-component本质上就是把Vue组件包装成class。那个年代的前端团队面试题里必然有“React生命周期顺序”“setState是同步还是异步”“this指向怎么处理”这些经典问题整个心智模型都是围绕“组件是一个对象对象有诞生、更新、销毁的过程”来建立的。我自己刚转React时也写过大量class组件。最典型的一个业务场景是列表页进入时拉数据、搜索条件变化时重新拉、离开页面时清掉定时器。我需要在componentDidMount里发请求在componentDidUpdate里对比搜索条件做二次请求在componentWillUnmount里清理逻辑一旦多了组件代码就像撒了一地的拼图。这还不是最难受的最难受的是多个组件共用一套数据请求逻辑时得用高阶组件HOC包裹一层或者用render props。嵌套层数一多你自己都分不清props是从哪一层传进来的。1.2 函数组件加Hooks为什么能接管局面React Hooks出现以后函数组件突然获得了完整的表达能力。useState解决状态useEffect解决副作用useContext解决跨层级共享useMemo和useCallback解决性能优化。最关键的还不是API数量变多了而是组合方式变了同一类逻辑可以从生命周期里“抠”出来聚合成一个自定义Hook。我经常用一个生活化的类比来解释这件事类组件像一个大工具箱所有工具都放在一个盒子里你找一把螺丝刀得翻半天Hooks像一堆独立的小抽屉每个抽屉只放一类工具要用哪个直接抽哪个还能把几个抽屉组合成一套新工具。这个解释带过很多刚入门的新人基本一听就懂。Vue 3的Composition API走的也是同一条路。Options API里data、computed、watch、methods被严格划分一个功能的代码要散落在至少四个区块里Composition API直接用setup函数把相关的数据、计算属性、监听器写在一起。所以别单纯说“React抛弃了类”这是整个前端框架层面的趋势只是React和Vue 3各自以不同的语法形态完成了同一个方向上的转变。1.3 注意类的减少不等于面向对象思想的消失这里需要郑重区分一件事JavaScript里的class和面向对象编程OOP思想并不是一码事。ES6的class本质是语法糖底层还是基于原型链。前端框架减少使用class不代表OOP被封杀了反而在业务模型、设计模式、服务端Node.js代码里class依然很常见。我后面会单独拿一节约来说这个问题因为你如果心里把这个概念混了很容易得出“前端不需要类和对象了”的片面结论。2. 拆解Class在前端框架里的具体痛点每个坑我都踩过2.1 this指向问题日常开发里最浪费时间的Bug来源类方法默认不绑定this。这是我在团队里讲解最多、也最哭笑不得的问题。class Counter extends React.Component { handleClick() { this.setState({ count: this.state.count 1 }); } render() { return button onClick{this.handleClick}点击/button; } }这段代码看似没有问题但用户一点按钮浏览器就报错Cannot read properties of undefined (reading setState)。原因很简单事件处理函数里的this不再是组件实例。解决办法不外乎三种在constructor里bind写箭头函数类属性或者在JSX里写箭头函数。三种方式各有坑bind让代码啰嗦箭头函数类属性依赖编译插件JSX里写箭头函数则每次渲染都会创建新函数可能破坏子组件的浅比较优化。这类问题在函数组件里直接消失了因为函数组件根本没有this。useState返回的setter是稳定的引用直接调用即可。不需要bind不需要担心this丢失。单就这一点就能省下新人至少两天的排错时间。2.2 生命周期方法把相关逻辑拆得七零八落类组件里数据的获取、更新、清理分散在不同的生命周期方法中。复用逻辑更是痛苦组件A的componentDidMount里有一段埋点逻辑组件B也想用你没法直接抽出去只能做成HOC或者render props。我印象很深的一个项目一个数据报表页面包含筛选、图表渲染、定时刷新、WebSocket推送。所有逻辑散落在componentDidMount、componentDidUpdate、componentWillUnmount里。后来加需求“筛选条件变化时重新初始化图表”我需要同时改三个地方漏改一个就出bug。最后重构成了函数组件只用了两个自定义Hook就整理干净了。这个痛点不是React独有。Vue 2的Options API同样存在data负责状态methods负责方法watch负责监听created和mounted负责初始化一个相对完整的功能被强制拆开。Composition API的出现本质上就是把“按生命周期组织代码”改成“按业务逻辑组织代码”。2.3 逻辑复用HOC vs 自定义Hook类组件的逻辑复用一直是个老大难问题。mixin在React社区不流行推荐方案是HOC和render props。HOC的本质是一个函数接收组件并返回新的组件function withFetch(WrappedComponent, url) { return class extends React.Component { componentDidMount() { fetch(url).then(res res.json()).then(data { this.setState({ data }); }); } render() { return WrappedComponent {...this.props} data{this.state.data} /; } }; }看起来很优雅但一旦多个HOC嵌套你很难定位props是从哪个HOC来的。你自己调试时打开DevTools看到的组件树是withFetch(withLoading(withErrorBoundary(MyComponent)))一层套一层心智负担很重。如果用自定义Hook逻辑就是直接的函数调用function useFetch(url) { const [data, setData] useState(null); useEffect(() { fetch(url).then(res res.json()).then(setData); }, [url]); return data; }然后在组件里就是一行调用。哪个Hook负责哪段逻辑一目了然依赖关系也清晰。这种组合模式相比HOC的嵌套无论是可读性还是可维护性都高出不少。2.4 函数更利于静态分析和打包优化还有一个容易被忽视的技术原因class的静态分析难度比普通函数大。比如某个class定义了一堆方法其中有些方法可能根本没被调用但打包工具难以安全地删除它们因为class的实例方法是挂在原型上的工具无法静态确定这些方法是否在未来被动态调用。相比之下普通函数配合tree-shaking凡是模块里没有被引用的导出分析工具可以放心去除。这个差异在大型项目里会被放大。前端框架本身为了性能和体积也会尽量避开class这种天然不利于优化的结构。框架设计者选择函数式方向不只是一个审美偏好背后有实实在在的工程考量。3. 实操对比同一个计数器两条技术路线差在哪3.1 用Class实现带加载状态和数据请求的列表为了不空谈我给一个具体例子。假设你要实现一个用户列表组件进入页面加载数据、显示loading、请求失败提示错误、点击某个用户跳转详情。用类组件写大概长这样class UserList extends React.Component { constructor(props) { super(props); this.state { users: [], loading: false, error: null }; } componentDidMount() { this.fetchUsers(); } componentDidUpdate(prevProps) { if (prevProps.query ! this.props.query) { this.fetchUsers(); } } componentWillUnmount() { this.mounted false; // 防止异步回调在卸载后setState } fetchUsers() { this.setState({ loading: true, error: null }); fetch(/api/users?query${this.props.query}) .then(res { if (!res.ok) throw new Error(请求失败); return res.json(); }) .then(users { if (this.mounted ! false) { this.setState({ users, loading: false }); } }) .catch(err { if (this.mounted ! false) { this.setState({ error: err.message, loading: false }); } }); } render() { const { users, loading, error } this.state; if (loading) return div加载中/div; if (error) return div出错了{error}/div; return ( ul {users.map(user ( li key{user.id} onClick{() this.props.onSelect(user.id)} {user.name} /li ))} /ul ); } }这段代码里有个细节componentWillUnmount里设置this.mounted false这个写法在当年的解决方案中非常常见用来防止setState发生在已卸载的组件上。但它本身就是一个很别扭的补丁它说明类组件在“异步操作生命周期管理”这件事上给开发者留下了额外的家务活。3.2 用函数加Hooks实现一样的场景明显更薄的代码同样的功能用Hooks重写function UserList({ query, onSelect }) { const [users, setUsers] useState([]); const [loading, setLoading] useState(false); const [error, setError] useState(null); useEffect(() { let ignore false; setLoading(true); setError(null); fetch(/api/users?query${query}) .then(res { if (!res.ok) throw new Error(请求失败); return res.json(); }) .then(data { if (!ignore) { setUsers(data); setLoading(false); } }) .catch(err { if (!ignore) { setError(err.message); setLoading(false); } }); return () { ignore true; }; }, [query]); if (loading) return div加载中/div; if (error) return div出错了{error}/div; return ( ul {users.map(user ( li key{user.id} onClick{() onSelect(user.id)} {user.name} /li ))} /ul ); }你仔细对比就能发现类组件里componentDidMount、componentDidUpdate、componentWillUnmount做的三件事情在函数组件里被合并成一个useEffect。useEffect的依赖数组[query]自动完成了“query变化时才重新请求”的判断清理函数替代了componentWillUnmount来防止异步setState。少写了很多防御性代码逻辑还是聚合的一整块。3.3 为什么团队最终选择整体迁移我朋友的公司有一个中型后台管理系统代码量大概三十万行早期全部是类组件。他们做了一次渐进式迁移新功能一律用函数组件加Hooks老组件在需求变更时顺手重构。半年后统计新代码占比超过了八成团队的bug率明显下降尤其是原来隔三差五出现的this相关报错基本绝迹了。他们做这个决定时考虑的不只是语法好丑这类问题我总结了三个实际动因协作成本降低。类组件里代码分布在不同生命周期新人接手时要不停上下滚动找逻辑块函数组件里一个功能的数据、监听、副作用全在一起代码评审时能沿着“函数执行流”往下看而不是沿着生命周期脑补执行流程。复用效率提升。公司内部沉淀了一批自定义HookuseAuth、useTable、useDownload新页面拼装页面像搭积木比之前引HOC再包一层清晰太多。招人容易了。现在前端市场上新人对函数组件和Hooks的掌握程度普遍高于class组件的各种奇技淫巧团队的培养成本明显下降。4. 类并没有消失那些仍在重度使用class的地方4.1 框架内部实现依然大量依赖类你可能觉得“类都被框架抛弃了”但真实情况是框架内核里class到处都是。React的Fiber架构中每个组件节点内部的数据结构就是一个Fiber对象Vue 3的响应式系统里有ReactiveEffect类Vue 3的组件实例、渲染上下文也都用class封装。框架的对外API越来越函数式但对内实现依旧用class来组织数据结构和算法。这一点很重要函数编程适合描述UI界面和组件逻辑但底层基础设施、类库设计、复杂状态管理class依然是主力。可以这么说类在“框架内部”没有变少变少的是在“业务组件层”的使用。4.2 TypeScript加持下class依然是领域建模的好工具如果做后端Node.js或者TypeScript全栈class依然是很好用的建模工具。比如定义一个User实体既有数据字段又有行为方法用class非常自然class User { constructor( public id: number, public name: string, private passwordHash: string ) {} isPasswordValid(plain: string): boolean { return hash(plain) this.passwordHash; } toSafeJSON() { return { id: this.id, name: this.name }; } }这种模型描述业务的表达能力是单纯接口加工具箱函数不好替代的。这跟用类写React组件完全是两码事。类的问题不在类本身而在于“让类去描述UI组件的生命周期”这件事并不合适。UI是状态映射成视图用函数天然合适业务实体是稳定的数据结构带方法用类天然合适。4.3 前端领域面向对象设计模式并没有过时设计模式中很多经典模式比如策略模式、观察者模式、状态模式在前端底层库中依然用得很多。状态管理库Pinia、Redux Toolkit的Store设计内部还是用了很多面向对象的思想只不过对外暴露的是函数式API。你要看懂这类库的源码还是得会读class。我自己带新人的经验是如果完全不懂OOP去看前端工程化工具链的源码会非常吃力。所以面试时我依然会问面向对象基础但考核点从“React里的class生命周期”转到了“解释原型链、继承、组合和设计模式的基本原则”上。一个只会在React组件里写class的人和真正理解面向对象的人对框架源码的理解深度会有本质差距。5. 常见问题与实操心得前端人关于类的十大疑问5.1 类组件会被彻底删除吗至少从React官方态度看短期内不会。类组件仍然被支持社区生态量太大直接remove不现实。但React文档已经开始把函数组件当作默认推荐新教程以Hooks为主。Vue 3同样保留Options API不过官方推荐Composition API。所以别担心你以前写的类组件会突然跑不了但要清醒一点新项目、新团队默认函数式路线是更稳妥的选择。5.2 老项目里的类组件要不要立刻重构我的建议是不搞“一刀切”式的重构。老的类组件如果运行稳定、测试覆盖足够、没有频繁变更需求就别动它。重构有一个天然风险——你可能把一个稳定模块改成了新bug。正确的做法是在给老组件提需求、改bug时顺手将这个小模块改成函数组件既验证了回归又完成了迁移风险可控。我曾经在重构一个数据大屏时一次性迁移了二十多个类组件结果引入了一个因为useEffect依赖遗漏导致的轮询bug在线上跑了一个星期才发现。自那以后我就坚持“小步快跑”的原则。5.3 JavaScript的class和Java的class是不是一回事这是新人最常混淆的点。JavaScript的class是语法糖底层是原型链没有真正的“类”概念Java的class是编译期的类型模板对象由类实例化而来。前端框架不用JavaScript的class针对的是这种“原型式语法糖”在大型UI组件场景下不顺手而Java这类强类型语言里class依然是中流砥柱。两者不能混为一谈。5.4 面试还被问类组件怎么办放心面试官问类组件考察的不再是“你还会写类组件吗”而是背后的三个底层认知是否理解组件从实例化到销毁的完整生命周期是否理解this在不同调用场景下的指向规则是否能从设计模式角度分析HOC和Hook的优劣。这几个问题背后对应的是你对JavaScript语言本质的理解跟你写不写class没有关系。5.5 我用class写Vue 3组件可以吗可以但没必要。Vue 3提供了defineComponent配合setup函数和TSX的写法社区里还有vue-class-component插件可以用class风格。不过既然Composition API已经成为主流推荐再坚持class风格意味着要额外维护装饰器配置生态支持和文档资料都会少很多。很多情况下你会发现自己一边用class装饰器一边又要在setup函数里写业务混着用更难受。6. 实战避坑想真正用好函数式组件这几个念头必须扭转6.1 别再用“生命周期”的脑回路去理解useEffect不少从类组件转过来的同事第一反应是把useEffect当componentDidMount来用往里面塞一堆逻辑依赖数组传个空数组完事。结果一旦业务需要依赖某个props就只能改成非空依赖然后莫名其妙踩了闭包陷阱。我建议的思考方式是不要把useEffect当成生命周期钩子而是当成“当依赖变化时同步执行的副作用调度器”。组件模板渲染完浏览器更新真实DOM后useEffect才会执行时机天然延后跟mounted的语义并不完全相同。想通这一点很多诡异问题就自然而然解释了。6.2 不要为了消除class而硬造自定义Hook新同事经常走向另一个极端把所有逻辑都封装成自定义Hook组件里只剩一行useXXX美其名曰“彻底函数式”。其实这是把之前HOC嵌套的问题换了个马甲过度抽象同样会增加理解成本。一个自定义Hook如果只在同一个组件里被调用一次且没有与其他Hook复用的需求我倾向于先让它留在组件函数内部等出现第二个使用场景再抽出来。过早抽象是比过度封装更隐蔽的坏味道。6.3 类仍然适合做基础设施和复杂状态模型如果你在开发一个复杂的前端业务库比如一个富文本编辑器、一个图形编辑器、一个音视频剪辑工具内部会有大量复杂的对象状态和操作类的封装往往是比一坨函数更清晰的选择。比如编辑器的文档模型、选区模型、操作历史栈天然是“状态加行为”的集合用class管理内部状态和方法的私有性比把一堆工具函数塞进模块里更容易维护。我自己做过一个画布标注工具核心模块就是用class写的画布引擎类负责节点管理、缩放、拖拽命令类负责撤销重做每个图形元素是一个独立的类实例。这个场景下状态机加命令模式用面向对象实践顺畅得多。后来给这个工具加了个React外壳外壳用函数组件加Hooks内部引擎还是class。两者结合得非常自然。所以说到底函数式与类不是仇人而是不同场景下的互相补充。6.4 写TypeScript时如何优雅地保留类的优势既然团队越来越TypeScript优先聊类的使用就绕不开TS。我认为最佳实践是UI层用函数组件加Hooks类型用interface或type定义props业务模型层可以放心用class配合private和readonly做访问控制框架边界层少用类继承多写纯工具函数。TypeScript的interface描述数据结构的能力很强配合泛型很多原本需要class的场景用interface加工具函数已经足够了。只有在需要封装私有状态、或者做动态分发时class才更有优势。7. 未来趋势前后端框架里的类不会被消灭但会进一步退到它该待的位置结合这几年的生态演进我认为接下来类在前端领域的发展方向是组件层继续函数化类作为基础设施继续默默支撑。React Server Components、Vue Vapor、Svelte这些新方向都在加强编译时优化和更精细的响应式更新而这些底层机制用class组织更顺手。另一方面AI辅助编程普及后不管是class还是函数写代码的颗粒度都在加快开发者把更多精力花在“组合”而不是“定义”上这也会让偏向函数式组合的范式更受欢迎。我从职业生涯里切身体会到一门技术能长期存在一定有它不可替代的价值一门技术被替换也不是因为它不好而是出现了更匹配场景的替代品。类的退让从本质上是前端把“UI描述”和“业务逻辑”的边界划分得更清晰了。UI是函数式的好土壤业务实体是面向对象的好土壤。认清这一点你就不会焦虑“类是不是没用了”而是可以在具体场景里选择最适合的工具。最后分享一个我们团队现在写React的统一规范组件一律函数式状态管理一律用Hook跨组件复用一律自研Hook只有底层库、工具链、领域模型才允许使用class。这套规则执行了两年代码质量明显稳定新老成员交接效率也高了很多。如果你正在为团队是否迁移到函数式而犹豫可以从一条新的页面组件约定开始慢慢让团队尝到甜头再逐步推广。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门