写过Vue2的人对Options API再熟悉不过了:data里定义数据,methods里写方法,computed里算属性,mounted里初始化。一个组件写下来,逻辑按类型分散在不同选项里。组件小的时候没问题,一旦复杂了------比如一个用户列表页同时管搜索、分页、行内编辑、批量操作------这些逻辑的代码全混在一起,data里一堆变量、methods里一堆函数,改一个功能得上下翻找半天。
组合式API(Composition API)就是来解决这个问题的。截至2026年8月,Vue最新稳定版是3.5.41,3.6已经进入RC阶段(Vapor Mode是这一版的亮点)。<script setup>从3.2开始就是推荐写法,生态已经完全成熟,没有理由再观望了。
两种API到底差在哪
Options API的组织方式是"按选项分类"。同一个功能的响应式数据、计算属性、方法、生命周期钩子,被拆散到不同选项里。组件越大,这种拆散带来的认知负担越重。
组合式API的组织方式是"按功能聚合"。一个功能的全部逻辑------数据、计算、方法、副作用------可以放在同一个地方。需要复用的时候,抽成一个函数,哪个组件要用就调哪个函数。
说白了,Options API是按代码类型组织,组合式API是按业务功能组织。后者更符合人类思考问题的方式。
script setup怎么写
<script setup>是组合式API的语法糖,也是最推荐的写法。对比一下就明白它省了多少事。
先看老写法(普通<script> + setup函数):
vue
<script>
import { ref, computed } from 'vue'
export default {
props: {
title: String
},
setup(props) {
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
return { count, double, increment }
}
}
</script>
再看<script setup>:
vue
<script setup>
import { ref, computed } from 'vue'
const props = defineProps({
title: String
})
const count = ref(0)
const double = computed(() => count.value * 2)
const increment = () => count.value++
</script>
省掉了export default、setup函数和return。顶层的变量和函数自动暴露给模板用,不用手动return。defineProps和defineEmits是编译宏,不需要导入,在<script setup>里直接用。
ref和reactive选哪个
这两个都是创建响应式数据的API,区别在于用法和适用场景。
ref包装基本类型和对象都行,访问值要加.value:
js
const count = ref(0)
count.value++ // 修改要加.value
const user = ref({ name: '张三', age: 25 })
user.value.name = '李四' // 也要.value
reactive只能包装对象,返回的是Proxy代理,访问不用加.value:
js
const state = reactive({ name: '张三', age: 25 })
state.name = '李四' // 直接改
模板里用ref不用加.value,Vue自动解包。但JS代码里必须加,这个容易忘,刚上手的时候踩过不少坑。
实际开发中的建议:基本类型用ref,对象类型用哪个都行。但有个坑要注意------reactive返回的对象解构后会失去响应性。const { name } = reactive(...)之后,name就是个普通字符串,改了不会触发更新。要解构又保持响应性,得用toRefs:
js
const state = reactive({ name: '张三', age: 25 })
const { name, age } = toRefs(state) // 解构成ref,保持响应性
团队里统一规范比较好,要么全用ref(心智负担低,都加.value),要么对象用reactive基本类型用ref。别混着来。
computed和watch
computed创建计算属性,跟Options API里的computed概念一样------基于响应式依赖缓存,依赖不变就不重算:
js
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed(() => `${firstName.value}${lastName.value}`)
watch监听响应式数据的变化,执行副作用。跟Options API的watch比,组合式API的watch更灵活,可以监听多个数据源:
js
// 监听单个ref
watch(count, (newVal, oldVal) => {
console.log(`从${oldVal}变成${newVal}`)
})
// 监听多个
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
// 任一变化都触发
})
// 监听reactive对象的属性,要写getter函数
watch(() => state.age, (newAge) => {
console.log(`年龄变成${newAge}`)
})
还有个watchEffect,它不需要指定监听谁,函数里用了什么响应式数据就自动监听什么。适合依赖关系复杂的场景,但追踪起来不如watch直观:
js
watchEffect(() => {
console.log(`${state.name}今年${state.age}岁`) // name或age变化都触发
})
watch是惰性的------数据变化才执行,可以拿到新旧值。watchEffect是立即执行的------一调用就跑一次,但拿不到旧值。按需选。
生命周期钩子变了什么
Options API里的mounted、created这些钩子,在组合式API里变成了onMounted、onBeforeMount等函数。beforeCreate和created的逻辑直接写在setup顶层就行,因为setup本身就是在这两个时机之间执行的。
对应关系很直接:
-
beforeCreate/created→ 直接写setup顶层代码 -
beforeMount→onBeforeMount -
mounted→onMounted -
beforeUpdate→onBeforeUpdate -
updated→onUpdated -
beforeUnmount→onBeforeUnmount -
unmounted→onUnmounted
js
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
console.log('组件挂载完成')
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
console.log('组件卸载')
window.removeEventListener('resize', handleResize) // 别忘了清理
})
composables:逻辑复用的正确姿势
组合式API最大的价值在逻辑复用。Options API时代复用逻辑靠Mixins,但Mixins的问题是命名冲突和数据来源不透明------三个Mixin混进来,某个变量是谁定义的根本看不出来。
composables就是一个约定:以use开头的函数,接收响应式数据,返回响应式数据和操作方法。看一个实际的例子------封装一个分页逻辑:
js
// usePagination.js
import { ref, computed } from 'vue'
export function usePagination(fetchFn, pageSize = 20) {
const currentPage = ref(1)
const total = ref(0)
const loading = ref(false)
const totalPages = computed(() => Math.ceil(total.value / pageSize))
async function goToPage(page) {
loading.value = true
const res = await fetchFn(page, pageSize)
total.value = res.total
currentPage.value = page
loading.value = false
}
function nextPage() {
if (currentPage.value < totalPages.value) {
goToPage(currentPage.value + 1)
}
}
return { currentPage, total, totalPages, loading, goToPage, nextPage }
}
组件里直接用:
vue
<script setup>
import { usePagination } from './usePagination'
const { currentPage, totalPages, loading, goToPage } = usePagination(fetchUsers, 20)
</script>
谁的数据谁的方法,一目了然。多个组件用同一个composable,各自维护独立的响应式状态,互不干扰。这比Mixin干净太多了。
TypeScript集成
组合式API跟TypeScript是天生一对。<script setup>加lang="ts"就能写TS:
vue
<script setup lang="ts">
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
}
const users = ref<User[]>([])
const keyword = ref('')
const filteredUsers = computed(() =>
users.value.filter(u => u.name.includes(keyword.value))
)
// defineProps带类型
const props = defineProps<{
title: string
count?: number
}>()
// defineEmits带类型
const emit = defineEmits<{
(e: 'update', user: User): void
(e: 'delete', id: number): void
}>()
</script>
defineProps和defineEmits支持泛型写法,类型推导完整。ref<User[]>([])能保证users里只能放User类型的数据,编译期就拦住类型错误。
从Options API迁移
已有项目从Options API迁到组合式API,不用一口气全改。Vue3支持两种API混用,可以渐进迁移。
策略上,新组件直接用<script setup>写,老组件在改动时顺手迁过来。优先迁的是那些逻辑复杂、用了多个Mixin的组件------这些组件迁完收益最大。
迁移时按功能拆分逻辑。一个Options API组件里可能有搜索、表格、表单三块逻辑,迁的时候把每块抽成独立的composable,<script setup>里只做组装。这个过程中你会发现,哪些逻辑耦合严重、哪些可以独立复用,代码结构会清晰很多。
注意this的问题。Options API里this指向组件实例,this.count、this.method()很常见。组合式API里没有this,所有数据和方法都是setup里的局部变量。如果老代码里大量用了this,迁移时得逐个替换,这也是迁移成本的主要来源。
还有一个容易忽略的点------v-model的写法变了。Vue3里v-model默认绑定的prop名从value改成了modelValue,事件从input改成了update:modelValue。支持多个v-model绑定到同一个组件,用v-model:title这样的语法。迁移时涉及自定义组件的地方要检查一遍。
迁移这事儿不用急,功能稳定优先。组合式API不是银弹,它解决的是逻辑组织和复用的问题,如果你的组件本来就简单,Options API写得也挺好,没必要为了迁而迁。