
很多开发者学习了Vue 3,却依然在用Vue 2的思维和方式编写代码。今天,我们来深入探讨一下,为什么组合式API(Composition API)不应被视为一个“可选项”,而是前端开发者技能进阶道路上的“必修课”。
思维的转变:从“按类型”到“按功能”
想象这样一个场景:你需要整理一个凌乱的办公桌。
采用Options API的思路,你会把所有的笔放进一个抽屉,所有的本子放进另一个抽屉,所有的充电线再单独整理。看起来井然有序,对吗?但当你需要完成“写周报”这个具体任务时,你不得不从笔筒里拿笔,从文件柜里找本子,再从线缆盒里翻出充电器。为了做一件事,你需要在多个地方来回翻找。
而Composition API的思路则截然不同:你会把“写周报”所需的所有工具——笔、本子、充电器——打包放在一个专用的收纳盒里。同样地,“画设计稿”的工具也会被放在另一个盒子中。你需要做什么,就直接拿出对应的盒子,一切都在手边,高效且专注。
这正是Vue组合式API的核心设计理念——按照逻辑功能来组织代码,而非按照选项类型(如data、methods)进行分类。这个概念听起来简单,但它所带来的思维转变,将彻底重塑你构建Vue应用的方式。
Options API在复杂场景下面临的挑战
我们先来看一个最基础的计数器组件,使用Options API编写:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
</script>
这段代码清晰、直观,对于简单组件而言没有任何问题。然而,当你的组件功能变得复杂,需要同时处理用户信息、搜索过滤、分页加载和表单校验等多个关注点时,代码结构可能会演变成这样:
+------------------+
| data() | ← count、userInfo、searchKey、pageNum、formData 全堆在一起
+------------------+
| methods | ← increment、fetchUser、handleSearch、loadMore、validate 全堆在一起
+------------------+
| computed | ← doubleCount、fullName、filteredList 全堆在一起
+------------------+
| watch | ← 监听count、监听searchKey、监听pageNum 全堆在一起
+------------------+
问题显而易见:一个独立的功能逻辑被强制拆分到了代码的不同区域。以“计数器”为例,它的状态 count 定义在 data 里,方法 increment 定义在 methods 里,衍生状态 doubleCount 定义在 computed 里,而监听 count 变化的逻辑则写在 watch 里。当你需要理解或修改“计数器”这个单一功能时,不得不在文件的多个部分之间来回跳转查找。
对于小型组件这或许尚可忍受,但一旦组件逻辑超过200行,维护起来就如同在“一碗面条里找一根特定的面条”,这就是Options API下常见的“面条式代码”问题,严重影响了代码的可读性和可维护性。
组合式API:重构你的代码组织方式
现在,让我们使用Composition API重写上面的计数器组件:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
</script>
观察这个变化,你会发现几个关键点:
第一,移除了 this。在Options API中,你必须通过 this.count、this.increment() 来访问和操作。而在组合式API中,一切都回归为普通的JavaScript变量和函数。这不仅消除了 this 上下文带来的困惑,也让代码更易于理解和测试。
第二,引入了 ref。你可以将 ref 理解为一个“响应式盒子”。普通的JavaScript变量变化时,Vue无法感知。但当你将值放入 ref() 创建的盒子中,Vue就能自动追踪其变化。访问或修改盒子内的值时,需要使用 .value 属性。
第三,使用了 <script setup>。这是Vue 3提供的一种编译时语法糖,它能极大简化组合式API的写法。在 <script setup> 标签内声明的顶层变量、函数、import引入,都可以直接在模板中使用,无需再通过 setup() 函数返回。
我们可以用一张结构图来直观对比两种API的思维差异:
Options API 的组织方式(按类型分) Composition API 的组织方式(按功能分)
┌─────────────────────┐ ┌─────────────────────┐
│ data: │ | 🔢 计数器功能: |
│ count │ | count = ref(0) |
│ searchKey │ | increment() |
│ userInfo │ | doubleCount |
├─────────────────────┤ | watch(count) |
│ methods: | ├─────────────────────┤
│ increment | | 🔍 搜索功能: |
│ handleSearch | | searchKey = ref(‘’)|
│ fetchUser | | handleSearch() |
├─────────────────────┤ | filteredList |
│ computed: | ├─────────────────────┤
│ doubleCount | | 👤 用户功能: |
│ filteredList | | userInfo = ref({}) |
│ fullName | | fetchUser() |
├─────────────────────┤ | fullName |
│ watch: | └─────────────────────┘
│ count |
│ searchKey |
└─────────────────────┘
左边是按照“抽屉类型”整理,右边是按照“具体事务”打包。哪种方式在维护复杂组件时更清晰、更高效,答案一目了然。
核心工具:computed、watch 与生命周期钩子
掌握了 ref 的基础用法后,我们再来学习组合式API中的三个核心工具。
computed:声明式的响应式依赖
如果你熟悉Excel,可以将 computed 理解为Vue中的“公式单元格”。你在A1单元格输入数值,在B1设置公式 =A1*2,那么A1的变化会自动触发B1的更新。computed 正是这样的响应式计算属性。
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
// doubleCount 会自动依赖于 count 的变化,无需手动维护同步逻辑
const doubleCount = computed(() => count.value * 2)
</script>
doubleCount 不是一个静态变量,而是一个响应式的计算属性。当 count 的值改变时,doubleCount 会自动重新计算并更新。你无需编写任何额外的监听或同步代码,Vue的响应式系统会处理这一切。
watch:响应状态变化的副作用
有些场景下,你不仅需要根据状态变化计算出新值,还需要在变化时执行一些带有副作用的操作,例如发起网络请求、记录日志或触发用户提示。这时,watch 就派上了用场。
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
watch(count, (newVal, oldVal) => {
console.log(`计数从 ${oldVal} 变成了 ${newVal}`)
// 实际应用场景示例:
// - 当搜索关键词变化时,发起防抖的接口请求
// - 当分页页码变化时,自动加载下一页数据
})
</script>
watch 的回调函数会接收新值和旧值作为参数,使你能够精确地响应数据变化。在实际项目中,watch 的典型应用包括:
- 监听搜索关键词的变化,执行防抖搜索。
- 监听路由参数的变化,重新获取页面数据。
- 监听表单字段的变化,进行实时校验。
onMounted:组件生命周期的接入点
在组件被挂载到DOM之后,我们通常需要执行一些初始化操作,例如请求初始数据、初始化第三方库或设置定时器。这正是生命周期钩子 onMounted 的用武之地。
<script setup>
import { ref, onMounted } from 'vue'
const message = ref(‘加载中...‘)
onMounted(() => {
// 模拟异步数据请求
setTimeout(() => {
message.value = ‘数据加载完成 ✅‘
}, 1000)
})
</script>
<template>
<p>{{ message }}</p>
</template>
在Options API中,你需要将这段逻辑写在 mounted() 选项内。而在组合式API中,onMounted 作为一个普通的函数被调用,你可以将它和它所要初始化的数据、方法紧密地写在一起,彻底告别在选项间“跳来跳去”的阅读体验。
组合式API的杀手锏:可组合函数(Composables)
前面的例子展示了如何在单个组件内更好地组织代码。然而,组合式API真正的威力在于实现逻辑在跨组件间的完美复用。
在Options API时代,逻辑复用主要依赖 mixins。但用过 mixins 的开发者大多深有体会,它存在几个显著的弊端:
Mixins 的三大坑:
┌──────────────────────────────────────────────┐
| ❌ 命名冲突:多个mixin可能定义同名属性/方法 |
| ❌ 来源不明:模板里用的变量,不知道来自哪个mixin |
| ❌ 隐式依赖:mixin之间可能存在看不见的依赖关系 |
└──────────────────────────────────────────────┘
组合式API的解决方案是 可组合函数(Composables) 。它本质上就是一个利用了Vue响应式API的普通JavaScript函数,返回响应式状态和方法。
让我们看一个具体的例子。假设项目中多个组件都需要“计数器”功能,我们可以将其封装:
// useCounter.js
import { ref } from ‘vue‘
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return { count, increment, decrement, reset }
}
在任何组件中,你都可以像使用普通函数一样使用它:
<script setup>
import { useCounter } from ‘./useCounter‘
// 用法一:使用默认初始值0
const { count, increment, decrement, reset } = useCounter()
// 用法二:自定义初始值,并可重命名返回值
const { count: score, increment: addScore } = useCounter(100)
</script>
看到了吗?这就是纯粹的JavaScript函数调用和解构赋值。没有任何黑魔法,没有隐式的属性注入。每一个在模板中使用的变量和方法,其来源都清晰可见,彻底解决了 mixins 的“来源不明”问题。
再看一个更贴近实际业务的例子——封装一个通用的数据请求Composable:
// useFetch.js
import { ref } from ‘vue‘
export function useFetch(url) {
const data = ref(null)
const loading = ref(true)
const error = ref(null)
fetch(url)
.then(res => res.json())
.then(json => {
data.value = json
})
.catch(err => {
error.value = err.message
})
.finally(() => {
loading.value = false
})
return { data, loading, error }
}
在组件中的使用变得极其简洁:
<script setup>
import { useFetch } from ‘./useFetch‘
const { data, loading, error } = useFetch(‘https://api.example.com/users‘)
</script>
<template>
<div v-if=“loading“>加载中…</div>
<div v-else-if=“error“>出错了:{{ error }}</div>
<ul v-else>
<li v-for=“user in data“ :key=“user.id“>{{ user.name }}</li>
</ul>
</template>
通过三行代码,我们就获得了数据、加载状态和错误处理。这种高度声明式且透明的逻辑复用方式,在可维护性和灵活性上远超传统的 mixins。
如何选择:一张图看懂应用场景
面对Options API和Composition API,你不必非此即彼。下图可以帮助你根据实际场景做出合理选择:
你的场景是什么?
|
▼
┌───────────────┐ 是 ┌──────────────────────┐
| 简单的小组件? ├────────→| Options API 完全够用 |
|(< 100行代码)| | 不用强行切换 |
└───────┬───────┘ └──────────────────────┘
| 否
▼
┌───────────────┐ 是 ┌──────────────────────┐
| 需要跨组件 ├────────→| Composables 是最优解 |
| 复用逻辑? | | 告别 mixins |
└───────┬───────┘ └──────────────────────┘
| 否
▼
┌───────────────┐ 是 ┌──────────────────────┐
| 组件逻辑复杂? ├────────→| Composition API |
|(多个关注点) | | 按功能分组,更好维护 |
└───────┬───────┘ └──────────────────────┘
| 否
▼
┌──────────────────────────┐
| 新项目 / 团队协作? |
| → 推荐 Composition API |
| 统一代码风格,降低协作成本|
└──────────────────────────┘
总结:一次面向未来的思维升级
许多开发者在面对“新API”时会感到焦虑:Options API是否即将被废弃?过去的学习成果是否会付诸东流?
这种担忧大可不必。Vue官方已明确承诺:Options API将作为稳定特性被长期维护,与Composition API共存。
尽管如此,我仍然给出以下建议:
- 对于Vue新手:建议直接从Composition API开始学习。它更贴近原生JavaScript的思维方式,理解核心概念后反而会觉得更直观、约束更少。
- 对于Vue 2迁移者:可以采用渐进式策略。在新功能或重构模块中使用Composition API,存量代码按计划逐步迁移。无需追求一刀切,但掌握这项能力是技术栈升级的必经之路。
- 对于团队技术决策者:在新启动的项目中,可以考虑统一采用 Composition API 配合
<script setup> 语法。这能显著提升代码库的一致性、可读性和长期可维护性,团队的协作效率也会因此受益。
现代前端工具链如 Vite 也为 Vue.js 的组合式开发提供了极佳的支持。组合式API的本质不仅仅是一次语法更新,更是一次代码组织范式的重大升级。它引导开发者从“机械的分类”转向“灵活的组装”,让前端代码真正像搭积木一样模块化和可复用。
这一步思维的跨越,对于每一位追求代码质量与开发效率的前端开发者而言,都至关重要。希望本文能帮助你更好地理解Vue 3组合式API的设计哲学与实践路径。如果你想与更多开发者交流此类技术心得,欢迎前往云栈社区的前端板块参与讨论。