最后更新:2026-01-19
用了大半年 Vue3,回头看 Vue2 Options API,有一种”当初怎么忍下来的”感觉。
不是说 Options API 不好,只是 Composition API 在代码组织上真的更灵活。以前一个组件功能多了,data、methods、computed、watch 分散在各处,逻辑关联的代码物理上却离得很远,读起来要不停地上下翻。
现在用 Composition API,相关逻辑可以放在一起,还能轻松抽成 composable 复用。
setup 语法糖
Vue3 有两种写法,标准写法和 <script setup> 语法糖。
标准写法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <script> import { ref, computed } from 'vue'
export default { setup() { const count = ref(0) const double = computed(() => count.value * 2)
function increment() { count.value++ }
// 需要手动 return 才能在模板中使用 return { count, double, increment } } } </script>
|
<script setup> 语法糖:
1 2 3 4 5 6 7 8 9 10 11
| <script setup> import { ref, computed } from 'vue'
const count = ref(0) const double = computed(() => count.value * 2)
function increment() { count.value++ } // 不需要 return,顶层变量自动暴露给模板 </script>
|
现在基本都用 <script setup>,代码量少,结构更清晰。后面的例子都用这种写法。
ref 还是 reactive
这是刚上手 Vue3 时最容易纠结的问题。
ref 用来创建响应式数据,基本类型必须用它,对象也可以用:
1 2 3 4 5 6 7 8 9 10
| <script setup> import { ref } from 'vue'
const count = ref(0) // 基本类型 const user = ref({ name: '' }) // 对象也可以
// 访问和修改都要 .value count.value++ user.value.name = 'Eric' </script>
|
reactive 只能用于对象,不需要 .value:
1 2 3 4 5 6 7 8 9 10 11 12
| <script setup> import { reactive } from 'vue'
const state = reactive({ count: 0, name: '' })
// 直接访问属性 state.count++ state.name = 'Eric' </script>
|
听起来 reactive 更方便,但它有几个坑:
1 2 3 4 5
| const { count } = state
state = { count: 1 }
|
我现在的习惯:基本类型用 ref,复杂对象状态也倾向于用 ref(ref({})),保持一致性,少踩坑。如果团队规范是 reactive,注意别随意解构就行。
computed 和 watch
computed 跟 Vue2 没太大区别,声明一个依赖其他数据的计算值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <script setup> import { ref, computed } from 'vue'
const firstName = ref('Ben') const lastName = ref('Cui')
// 只读 computed const fullName = computed(() => `${firstName.value} ${lastName.value}`)
// 可写 computed const fullName = computed({ get: () => `${firstName.value} ${lastName.value}`, set: (val) => { const parts = val.split(' ') firstName.value = parts[0] lastName.value = parts[1] } }) </script>
|
watch 监听数据变化执行副作用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <script setup> import { ref, watch, watchEffect } from 'vue'
const keyword = ref('')
// 监听单个值 watch(keyword, (newVal, oldVal) => { console.log('搜索词变了:', newVal) })
// 监听对象深层 const user = ref({ name: '', age: 0 }) watch(user, (newVal) => { // deep 监听需要显式开启 }, { deep: true })
// 监听对象的某个属性,用 getter 函数 watch( () => user.value.name, (name) => { console.log('名字变了:', name) } )
|
watchEffect 不用指定监听谁,自动追踪内部用到的响应式数据:
1 2 3 4 5 6 7 8 9 10 11
| <script setup> import { ref, watchEffect } from 'vue'
const id = ref(1)
// 自动追踪 id,id 变了就重新执行 watchEffect(async () => { const data = await fetchUser(id.value) // ... }) </script>
|
watchEffect 写起来简单,但有时候追踪范围不明确,调试起来麻烦。我一般复杂情况还是用 watch 明确指定。
组合函数(Composable)
这是 Composition API 最大的亮点。把相关逻辑抽成独立函数,在组件之间复用。
比如封装一个分页逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| import { ref, computed } from 'vue'
export function usePagination(fetchFn, { pageSize = 10 } = {}) { const currentPage = ref(1) const total = ref(0) const list = ref([]) const loading = ref(false)
const totalPages = computed(() => Math.ceil(total.value / pageSize))
async function fetchData() { loading.value = true try { const res = await fetchFn({ page: currentPage.value, pageSize }) list.value = res.list total.value = res.total } finally { loading.value = false } }
function changePage(page) { currentPage.value = page fetchData() }
fetchData()
return { list, loading, currentPage, total, totalPages, changePage, refresh: fetchData, } }
|
在组件里使用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script setup> import { usePagination } from '@/composables/usePagination' import { getUserList } from '@/api/user'
const { list, loading, currentPage, total, changePage } = usePagination(getUserList) </script>
<template> <div v-loading="loading"> <UserCard v-for="user in list" :key="user.id" :user="user" /> <Pagination :current="currentPage" :total="total" @change="changePage" /> </div> </template>
|
这种封装方式比 Vue2 的 mixin 好在哪里:
- 来源清晰,
usePagination 返回什么一目了然,不像 mixin 会”神秘地”注入属性
- 没有命名冲突,多个 composable 在同一组件里共存也没问题
- 可以传参,更灵活
生命周期
Vue3 生命周期钩子改成了函数形式,在 setup 里调用:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script setup> import { onMounted, onUnmounted, onBeforeUnmount } from 'vue'
onMounted(() => { console.log('组件挂载了') window.addEventListener('resize', handleResize) })
onBeforeUnmount(() => { // 记得清理副作用 window.removeEventListener('resize', handleResize) }) </script>
|
beforeCreate 和 created 在 Composition API 里不存在,setup 函数本身就相当于这两个时机。
一些踩过的坑
响应式丢失:toRefs 可以解构 reactive 对象时保持响应式
1 2 3 4 5 6
| import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0, name: '' })
const { count, name } = toRefs(state)
|
异步 setup:<script setup> 里用 async/await 需要配合 Suspense 组件,否则模板会在数据准备好前渲染
1 2 3 4 5
| <!-- 父组件用 Suspense 包裹 --> <Suspense> <AsyncChild /> <template #fallback>加载中...</template> </Suspense>
|
模板 ref:获取 DOM 节点的方式变了
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script setup> import { ref, onMounted } from 'vue'
const inputRef = ref(null) // 名字要和模板里的 ref 属性一致
onMounted(() => { inputRef.value?.focus() }) </script>
<template> <input ref="inputRef" /> </template>
|
总体来说 Vue3 迁移成本没我想象的高,新项目直接上就行。老项目的话,Vue3 兼容了 Options API,可以渐进式迁移,不用一次全改。