资讯动态

Vue 3组合式API对比Options API:代码组织与逻辑复用优势详解

发布时间:2026/9/29 17:49:44 来源:尧图企业网站定制
1. 在真实项目里摸爬滚打后才看明白两者差在哪儿先说个场景。你接了一个运营后台项目某个订单列表页从最初的三百行代码被产品需求堆到了一千二百行。这个时候在 OptionsAPI 里找导出的字段是什么感觉你得反复滚动到 data、computed、watch、methods 四个区块之间来回跳。更难受的是这四个区块之间靠什么关联靠 this靠属性名靠你脑子里记住的字段A设置在 data 里计算属性B负责加工它方法C在 methods 里调用它们。如果某一天你不太记得方法名搜索export可能搜出来八个结果你得逐个判断到底哪个是那个列表导出的方法。这就是 OptionsAPI 的先天性格它是按数据性质组织代码的不是按业务逻辑组织代码的。而业务在真实世界里从来不会按数据性质走它是一整条线先查列表数据、再处理筛选条件、然后应对分页变化、最后把结果渲染出来。这条线在 OptionsAPI 里被硬生生切成了 data 一块、computed 一块、methods 一块就跟把一条流水线拆成不同楼层你干完这道工序还得坐电梯去另一层找下一道工序。Composition API 的出现本质上是在说我们能不能不按数据类型划分而是按这个功能需要哪些数据、哪些计算、哪些方法来划分。用组合式 API 写同一个订单列表你会把列表数据、加载状态、筛选条件、获取列表的方法、重置筛选的方法、还有相关的 computed全都放进同一个逻辑块里。该功能需要的东西聚在一起看起来就像一条完整的流水线摆在眼前。这个体感上的差别恰恰是 Vue3 的 Composition API 相对 OptionsAPI 最核心、也最容易被忽略的价值。不过这里我得泼一盆冷水如果你觉得哦就是把代码按功能重新摆了一下那还是没抓住重点。Composition API 不是一种代码排版风格它是一种从组件选项容器转向逻辑表达单元的机制变化。后者带来的东西包括真正的逻辑复用、更可靠的类型推导、更明确的依赖关系远比代码排列整齐重要得多。2. 用同一段代码说话列表页在两种写法下的样子我见过太多介绍文章只给结论不给证据结果新手看完除了记住Composition API 更好这句话其他什么都没记住。我们直接上一段稍微具体一点的代码实现一个常见的场景一个需要搜索、筛选、分页的列表页。先用 OptionsAPI。export default { name: OrderList, data() { return { page: 1, pageSize: 10, keyword: , status: , list: [], total: 0, loading: false } }, computed: { filteredParams() { const params { page: this.page, pageSize: this.pageSize } if (this.keyword) params.keyword this.keyword.trim() if (this.status) params.status this.status return params } }, watch: { keyword() { this.page 1 this.fetchList() }, status() { this.page 1 this.fetchList() } }, methods: { async fetchList() { this.loading true try { const res await fetch(/api/orders, { method: POST, body: JSON.stringify(this.filteredParams) }).then(r r.json()) this.list res.data.list this.total res.data.total } finally { this.loading false } }, handlePageChange(newPage) { this.page newPage this.fetchList() }, handleReset() { this.keyword this.status this.page 1 this.fetchList() } }, mounted() { this.fetchList() } }这段代码不长但你已经能感受到分流keyword、status、page 这些数据在 data 里fetchList 在 methods 里filteredParams 又独立在 computed 里。当你要给这段逻辑新增一个时间范围筛选时你要动三个地方data 里加开始时间和结束时间、computed 里加参数拼接、methods 里把新字段塞进 fetchList 的调用。三个分隔区来回改这还算是小型场景。真到了复杂业务组件代码块会拉得很长这种割裂感会被放大。再看同样功能用 Composition API 配合script setup怎么写。script setup import { ref, reactive, computed, watch, onMounted } from vue const page ref(1) const pageSize ref(10) const keyword ref() const status ref() const list ref([]) const total ref(0) const loading ref(false) const filteredParams computed(() { const params { page: page.value, pageSize: pageSize.value } if (keyword.value) params.keyword keyword.value.trim() if (status.value) params.status status.value return params }) async function fetchList() { loading.value true try { const res await fetch(/api/orders, { method: POST, body: JSON.stringify(filteredParams.value) }).then(r r.json()) list.value res.data.list total.value res.data.total } finally { loading.value false } } watch([keyword, status], () { page.value 1 fetchList() }) function handlePageChange(newPage) { page.value newPage fetchList() } function handleReset() { keyword.value status.value page.value 1 fetchList() } onMounted(fetchList) /script你对比一下功能完全等价但组合式版本把列表相关的状态和列表相关的函数放在同一个作用域里面它们天然共享作用域变量不再需要 this 的中转。这就是我说的机制变化它不是把代码换个位置那么简单而是取消了所有方法挂到组件实例上这个强制要求。方法就是普通函数数据就是普通变量全都在一个作用域里自然流动。这种模型对于理解代码的人、审查代码的人、接手代码的人来说心智负担直接降一档。3. 从 Mixin 到自定义 Hooks逻辑复用方式终于从补丁变成了原生方案这个点我认为是 Composition API 最甜蜜的果实。在 OptionsAPI 时代我们做逻辑复用靠什么靠 Mixin。Mixin 这个方案有几个一直没解决的硬伤。第一个是来源不明。假设项目里有mixins: [paginationMixin, statusMixin, auditMixin]模板里突然出现一个visibleFlag字段你根本不知道这个字段是从哪个 Mixin 里注入进来的。IDE 跳转也经常失效只能靠全局搜索。第二个是命名冲突。两个 Mixin 如果恰好有同名属性或方法后者覆盖前者而且 Vue 在运行时不会对你发出任何警告。这个 bug 的隐蔽性极高。第三个是隐式数据耦合Mixin 内部的 methods 会依赖组件里的某些 data 字段你复制一个 Mixin 到另一个组件里经常出现方法有了但数据没有的尴尬需要在组件里额外补字段补完才发现另一处又冲突了。可以说 Mixin 这套复用机制长期处于一种能用但很疼的状态。Composition API 推出的自定义 Hook通常叫 composable从根上改变了这个局面。它首先是一个普通函数函数天然接受参数、返回结果所有数据依赖都通过参数传入不再凭空注入组件的命名空间。其次返回的每一个响应式变量和函数调用方都清楚它来自哪里因为是你亲手调用的这个函数。我们看一个实际的例子。// usePagination.js import { ref, computed } from vue export function usePagination({ getList, initialTotal 0 } {}) { const page ref(1) const pageSize ref(10) const total ref(initialTotal) const totalPages computed(() Math.max(1, Math.ceil(total.value / pageSize.value))) async function fetchPage() { const res await getList({ page: page.value, pageSize: pageSize.value }) total.value res.total return res.list } function reset() { page.value 1 return fetchPage() } function setPage(val) { page.value val return fetchPage() } return { page, pageSize, total, totalPages, fetchPage, reset, setPage } }然后在任意组件里我只需要三行就能把分页能力接进来。script setup import { usePagination } from /hooks/usePagination const { page, pageSize, total, totalPages, reset, setPage } usePagination({ getList: fetchOrderList }) async function fetchOrderList({ page, pageSize }) { // ... } /script不同组件之间如果要共享同一个加载态 Hook写法也几乎一样。比如useLoading、useRequest、useDebounce、useLocalStorage全都是同一套思路。这种复用方式的优势不仅在于代码量减少更在于我明确知道这一个 Hook 管的是什么逻辑。团队在 code review 时看到const { list, loading, refresh } useListApi(/api/orders)不需要深入函数内部就已经能大致猜到整个页面的数据流走向。这种可读性才是工程上最值钱的东西。4. 依赖关系越发清晰watchEffect 与 watch 各自的本职工作经常看到 Vue3 新人困惑 watch 和 watchEffect 到底该用哪个。其实放到 Composition API 的语境下这个选择变得非常自然因为它们俩响应的是两种不同的开发需求。先说说 Vue3 的响应式底层Proxy 代理。在 OptionsAPI 里数据通过data()返回后由 Vue 遍历劫持这个时代用的是Object.defineProperty你无法精确追踪新增属性、删除属性改数组下标也得走$set。到了 Vue3ref底层用对象包装了一个 valuereactive底层用new Proxy代理整个目标对象。这个变化带来的直接后果是追踪变得精确且中立。你可以对一个对象新增属性、删除属性、对数组按下标赋值响应式系统都能捕获到不需要再记各种特殊的 API 了。在这个机制上Composition API 的 watch 用法和 OptionsAPI 基本一致就是明确指定我观察谁然后执行副作用。而 watchEffect 则是我不指定具体看谁只要这个回调里用到了任何响应式数据它们一变我就重跑。这个概念如果你只听不做会觉得 watchEffect 太省事了什么都替你盯着。但它最大的问题是你不知道它到底依赖了哪些东西尤其在代码稍微复杂一点之后你很难预判回调何时触发。所以我的原则很简单如果是某条件变化后执行一个明确动作比如筛选条件变化后重新拉列表用 watch并且精确指定 watch 的来源如果是立即执行一次并且任何相关数据变化时都同步状态的副作用例如根据当前 props 和状态推导出 document.title或者往 localStorage 里同步写入一份状态副本用 watchEffect 更省心。这里有个很容易忽略的细节watchEffect 一上来就会执行一次回调所以直接把初始化赋值和后续更新同步两件事合并了这在 OptionsAPI 里你得写 mounted 加 watch 两处。再强调一个实战中特别容易误解的东西computed 的依赖收集。在 Vue3 里 computed 返回的是一个ComputedRef对象使用时注意.value。有些人在 computed 内部用了不是响应式的普通变量期望它自动更新结果当然不行。而有些人反过来在 computed 内部写了一大堆副作用比如发请求、改别的 ref这其实是滥用了 computed它本质应该是纯计算。副作用请放到 watch 或 watchEffect 里或者放到事件处理函数中。这个原则不仅 Vue 适用React 里对 memo 的要求也几乎一模一样。理解了这个你才算摸到了响应式框架的通用心法。5. 代码复用之外的另一块版图TypeScript 类型推导与模块化边界Composition API 另一个被低估的优势是它对 TypeScript 的亲和度。Vue3 本身就是用 TypeScript 重写的Composition API 的所有 API 签名都自带完善类型定义这一点和 OptionsAPI 有天壤之别。在 OptionsAPI 时代写 TypeScript 经常要靠装饰器提案或者类组件库来维持类型体验并不好。到了 Composition API一个ref(0)自动推导出Refnumber一个reactive({ name: , age: 0 })自动推导出对应对象类型等于把类型系统直接嵌入到了代码里。如果你定义一个小型 Hook还能借助泛型把类型传进去这样调用方拿到的返回值也完全带类型。这种类型推导带来的强约束不只是解决拼错属性名这种低级问题更体现在大型团队协作上。一个组件被五个人前后修改只要你把数据结构用类型定义清楚任何新增字段在编译阶段就会被揪出来。比如说你定义了interface OrderItem { id: number; amount: number }在模板里误写item.amtTypeScript 会直接报错。在 OptionsAPI 的 this 类型推断里这样的检查很难做到同样的深度和自然度。不过类型推导背后还有一层值得你重视的东西模块边界。Composition API 把逻辑拆分成了无数个小函数、小 Hook这意味着你可以把代码从大组件文件里解放出来装进一个个独立文件里。这对代码审查、单测、重构都是巨大的帮助。比如你做了一个useOrderStatus它管状态流转、超时自动关闭、异常状态下发提醒。这个 Hook 量级大约五十行可以被单独测试、单独复用与组件 UI 完全解耦。而 OptionsAPI 里的业务逻辑分散在 mixin 和组件方法里你想单独测某个功能线需要先构造一整个组件实例测试成本高出一个量级。逻辑可测试性和模块边界在工程上的价值往往比你在 demo 里感受到的更加实际。6. 适用场景判断题哪些项目真的应该迁哪些项目暂时别动现在回到最实际的问题我一个 Vue2 项目到底要不要迁到 Composition API。我的建议是分场景判断别盲目跟风。先说不怎么需要迁的情况。如果你的项目是短生命周期的小工具页面比如内部的一个报表页面总共就一个组件没有跨组件共享逻辑也没有复杂状态流几十行代码就写完了。这种情况下你用 OptionsAPI 写没问题因为它的心智模型简单、文档多、团队里随便谁都能改。项目成本收益比最优解往往不是最先进的技术而是在该复杂度下最省事的技术。强行引入 Composition API反而要给每个 ref 手动管理增加了思维负担。还有一种情况是项目已经稳定运行了好几年现有代码全是 OptionsAPI而团队规模很小、项目没有新增复杂逻辑的迹象那就没有必要做一次大迁移因为迁移本身并不产生用户价值。再说适合迁的场景我做一个粗略的判断表给你参考。场景特征建议组件代码超过五百行经常在 data/methods/computed 之间反复跳强烈建议迁 Composition API存在多个组件共享同一套逻辑分页、列表加载、表单校验建议用自定义 Hook 重构项目需要使用 TypeScript且类型要求严格建议迁移收益非常明显团队规模大多人并行维护同一模块建议至少新代码采用 Composition API仅仅是单个小页面逻辑非常简单可以不迁团队所有成员对 OptionsAPI 已经极熟短期学习成本偏高建议渐进式引入先从一个页面试点如果是老项目想渐进式迁移我给一条比较稳的路径。第一步把现有逻辑拆分成独立的 composable 函数注意这个步骤可以不动组件的 OptionsAPI只是把我们想要的新逻辑抽象成 Hook放进hooks目录。第二步在组件里新增的代码用script setup方式写而不是先把老代码一次性改写。第三步通过一段时间的观察把 OptionAPI 里的方法迁移到 Hook 中最后逐步清空 OptionsAPI 的区块。这样迁移的风险会被控制在一个小范围内每次发布都能测试不会出现一夜之间重构完毕的大事故。7. 关于 setup 语法糖和响应式丢失的坑我踩过也复盘过最后聊聊script setup语法糖。很多人第一次接触 Composition API 被它的this消失搞懵了 —— 没错在script setup里你不能再使用this所有方法、变量都必须声明在 setup 作用域内然后模板直接绑名字。这是一个非常干净的模型没有组件实例混入没有隐性代理模板能访问的变量全部是显式声明的。这种显式看似啰嗦实则是安全感的来源。但正是因为 setup 作用域下的代码如此普通你失去了 Vue 在 OptionsAPI 里自动绑定 this 的能力因此特别容易碰上一个经典问题响应式丢失。举个例子你写const { list, loading } useListApi()如果这个 Hook 内部是用reactive({ list: [], loading: false })定义的那么解构出来之后list和loading就不再具有响应性。为什么因为 reactive 代理的是这个对象本身你把属性取出来赋值给普通变量这个变量的值只是当时那个时刻的快照后续改动跟它没有关系。解决方案有两个要么内部用ref定义每个字段因为 ref 是用.value访问的解构出来的仍是一个 Ref 对象具有响应性要么对 reactive 对象用toRefs转换成一组 ref再解构。再分享一个 project 级的经验不要在script setup里把所有的逻辑都堆在顶层。我见过有人把整个页面三百行逻辑全塞进 setup 里这虽然也是写在同一个函数作用域但从组织性上还不如 OptionsAPI 的方案。正确做法是页面里的通用逻辑尽量抽象成 Hook组件顶层不过是对 Hook 的装配。比如订单列表页顶层可以只调useOrderList()、useOrderFilter()、usePaginator()三个 Hook页面的 setup 只有三五行装配代码其他细节全部下沉到 Hook 内部。这样组件读起来像目录逻辑在各自文件里完整呈现团队协作成本最低。还有一个真实的坑是事件监听器的清理。在 OptionsAPI 里我们在 beforeDestroy 生命周期注销全局事件。到 Composition API 里则是onUnmounted(() { window.removeEventListener(scroll, handler) })。但很多人写 Hook 时容易忘记在内部主动注册清理逻辑导致组件卸载后监听器还挂在 window 上造成内存泄漏。所以我个人在写任何 Hook 时都会在 Hook 内部把所有需要清理的资源一次性处理好确保外部调用方不需要额外操心。这就像写一个工具函数连文档一起交付属于工程素养问题。另外在异步函数里使用 ref 也要小心。比如在 await 之后修改 ref 的值代码是可运行的但如果组件已经卸载这个赋值操作就会被忽略Vue3 默认不会报错。这种情况可以借助一个简单的isMounted标志位来判断或者在 Hook 里维护一个onUnmounted清理的 flag。这个细节在长时间运行的页面上很容易变成隐患。最后补充一个我在培训时反复强调的建议不要为了看起来高级而强行使用 Composition API。它确实是 Vue3 的未来但比起它代表的技术方向你更应该关注的是它解决的具体问题——代码组织混乱、逻辑复用困难、类型推导缺失。如果你当前的项目没有这些问题用 OptionsAPI 并不可耻。如果项目确实有这些病那就大胆迁移从一个小模块开始用真实的代码体感去对比。等你真的把一个三百行组件拆成三个 Hook、再把它们移植到另一个页面只用了十行代码时你自然就明白这个设计为什么值得存在。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑