导语
维护一个 Vue 2 老项目时,你大概遇到过这样的场景:一个组件几百行,data、methods、computed、watch 被选项区块切得七零八落,相关的逻辑散落在五六个区块里。想复用一段"计数器 + 定时刷新"的逻辑,只能复制粘贴,或者祭出 mixins------结果命名冲突、来源不清。Vue 3 的 Composition API 正是为解决这类问题而来。本文用一条渐进式路径,带你从 Options API 平滑迁移到 <script setup>,不重写、不掉档。
摘要 :本文梳理从 Options API 迁移到 Composition API 的完整路径:先用
setup()桥接混用,再逐项改写各选项区块,接着落地<script setup>语法糖,重点拆解ref/reactive与解构陷阱,最后用 composables 替代 mixins 做逻辑复用。附对照表、迁移流程图与可运行示例。
文章目录
-
- 导语
- [一、为什么要迁移:Options API 的痛点与 Composition API 的优势](#一、为什么要迁移:Options API 的痛点与 Composition API 的优势)
- [二、渐进式迁移策略:用 setup() 桥接,新老 API 混用](#二、渐进式迁移策略:用 setup() 桥接,新老 API 混用)
- [三、语法逐项对照:Options 各区块如何改写为 Composition](#三、语法逐项对照:Options 各区块如何改写为 Composition)
- [四、`<script setup>` 语法糖:更简洁的组件写法](#四、
<script setup>语法糖:更简洁的组件写法) - [五、响应式核心与高频坑:ref vs reactive 与解构陷阱](#五、响应式核心与高频坑:ref vs reactive 与解构陷阱)
- [六、逻辑复用升级:从 mixins 到 composables](#六、逻辑复用升级:从 mixins 到 composables)
- 总结
一、为什么要迁移:Options API 的痛点与 Composition API 的优势
Composition API(组合式 API) 是框架提供的、基于函数的逻辑组织方式,把"按选项分类"换成"按逻辑关注点聚合"。
Options API 把代码按 data、methods、computed、watch、生命周期五大块平铺。组件小的时候清晰,一旦逻辑变多,同一功能的代码就被拆进不同区块,阅读时要上下反复跳转。更麻烦的是复用:Options 时代主要靠 mixins,多个 mixins 合并后,同名属性会静默覆盖,调试时很难追溯来源。
| 维度 | Options API | Composition API |
|---|---|---|
| 逻辑组织 | 按选项类型平铺 | 按逻辑关注点聚合 |
| 复用方式 | mixins(易冲突) | composables(来源清晰) |
| 类型推导 | 较弱 | 配合 TS 更友好 |
| 代码跳转 | 需跨区块查找 | 相关逻辑集中一处 |
官方 FAQ 也点明两个核心收益:better logic reuse(更好的逻辑复用)与 more flexible code organization(更灵活的组织方式)参考 官方文档。需要强调的是,迁移不是"二选一"------框架同时兼容两种风格,老代码可以继续跑。

二、渐进式迁移策略:用 setup() 桥接,新老 API 混用
最稳妥的迁移不是推倒重来,而是在现有 Options 组件里用 setup() 引入 Composition 逻辑。框架允许在 Options 组件内声明 setup(),它的返回值会合并进组件实例 ,与 data、methods 共存。这样你可以只把最痛的那段逻辑改成 Composition,其余暂不动。
vue
<script>
import { ref, onMounted } from 'vue'
export default {
props: { title: String },
data() {
return { count: 0 }
},
setup(props) {
// Composition 逻辑:响应式引用 + 生命周期
const message = ref('Hello Composition')
onMounted(() => {
console.log('setup 内注册的生命周期已触发')
})
// 返回的对象会合并进组件实例,模板可直接使用
return { message }
}
}
</script>
上面这个组件,data 里的 count 和 setup 里的 message 在模板中都能直接用,互不干扰。这是新老混用的关键过渡形态。

推荐的迁移路径如下,每一步都可独立提交、独立验证:

三、语法逐项对照:Options 各区块如何改写为 Composition
进入全量改写阶段,逐一把选项区块映射到 Composition 函数。下面先给一张速查表,再给改写前后对比。
| Options API | Composition API |
|---|---|
data() |
ref() / reactive() |
methods |
普通函数 |
computed |
computed() |
watch / watch:{} |
watch() / watchEffect() |
created / mounted 等 |
onMounted() 等 onXxx |
props |
defineProps()(<script setup>) |
emits |
defineEmits()(<script setup>) |
改写前(纯 Options):
vue
<script>
export default {
data() {
return { count: 0 }
},
computed: {
doubleCount() { return this.count * 2 }
},
watch: {
count(newVal) { console.log('count changed', newVal) }
},
methods: {
increment() { this.count++ }
},
mounted() {
console.log('mounted')
}
}
</script>
改写后(Composition,仍用 setup() 导出):
vue
<script>
import { ref, computed, watch, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
watch(count, (newVal) => console.log('count changed', newVal))
const increment = () => { count.value++ }
onMounted(() => console.log('mounted'))
return { count, doubleCount, increment }
}
}
</script>
注意 ref 在 JS 里要用 .value 读写,模板里自动解包;this 在 setup() 中已不存在,所有状态通过变量直接引用。

四、<script setup> 语法糖:更简洁的组件写法
当组件完全转向 Composition,每次都写 setup() + return 很啰嗦。于是有了 <script setup> 这个编译时语法糖:在 <script> 上加 setup 属性,顶层绑定自动暴露给模板,无需 return。
它的几个关键点:
- 顶层声明的变量/函数自动暴露到模板;
- 模板里的
ref自动解包 ,不用.value; defineProps/defineEmits是编译宏,无需 import;defineOptions(Vue 3.3+)可在<script setup>里声明组件名等选项。
完整 SFC 示例:
vue
<script setup>
import { ref, computed, onMounted } from 'vue'
const props = defineProps({ title: String })
const emit = defineEmits(['change'])
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
emit('change', count.value)
}
onMounted(() => console.log('组件已挂载'))
</script>
<template>
<h2>{{ title }}</h2>
<p>count: {{ count }} / double: {{ doubleCount }}</p>
<button @click="increment">+1</button>
</template>
这就是生产环境最常见的写法:逻辑集中在 <script setup>,模板只负责渲染。

五、响应式核心与高频坑:ref vs reactive 与解构陷阱
响应式(reactivity) 指数据变化自动驱动视图更新。框架提供两个核心 API:ref(可包装任意类型,推荐作为主力)与 reactive(仅限对象/数组类型,底层是 ES Proxy)。
官方建议以 ref 为主 API。原因正藏在下一个坑里:reactive 返回的是 Proxy 对象,一旦解构就会丢失响应式。
javascript
// ❌ 错误:直接解构 reactive,响应式丢失
import { reactive } from 'vue'
const state = reactive({ x: 0, y: 0 })
let { x, y } = state // x、y 变成普通值,修改不再触发更新
javascript
// ✅ 正确:用 toRefs 保持响应式
import { reactive, toRefs } from 'vue'
const state = reactive({ x: 0, y: 0 })
const { x, y } = toRefs(state) // x、y 仍是 ref,修改会驱动更新
另一个细节:ref 放在数组或 reactive 对象里时,在 JS 中不会自动解包 ,仍需 .value。所以日常开发更省心的选择是用 ref 统一管理,配合 toRefs 在需要解构时保活。
| 对比项 | ref | reactive |
|---|---|---|
| 支持类型 | 任意(基本/对象) | 仅对象/数组 |
| 访问方式 | .value(JS 中) |
直接访问属性 |
| 解构 | 用 toRefs 保活 |
直接解构会丢失响应式 |
| 官方定位 | 推荐主力 API | 对象场景的便捷封装 |
关于响应式的底层原理,可进一步阅读 官方响应式文档。

六、逻辑复用升级:从 mixins 到 composables
composables(组合式函数) 指以 use 开头命名的普通 JS 函数,内部用 Composition API 封装一段可复用状态与逻辑。它替代了 mixins:没有命名冲突,每个状态的来源都清晰可见。
一个最小可用的 useCounter:
javascript
// composables/useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initial = 0, step = 1) {
const count = ref(initial)
const double = computed(() => count.value * 2)
const increment = () => { count.value += step }
const decrement = () => { count.value -= step }
return { count, double, increment, decrement }
}
在组件里复用,多个组件之间状态互不污染:
vue
<script setup>
import { useCounter } from './composables/useCounter'
const { count, double, increment, decrement } = useCounter(0, 1)
</script>
<template>
<p>{{ count }} / {{ double }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</template>
相比 mixins 的"隐式合并",composables 显式返回、显式解构,来源一目了然。生态上,VueUse 提供了大量开箱即用的组合函数(如鼠标位置、剪贴板、防抖等),可以直接引入,省去重复造轮子。

总结
迁移的本质不是"换写法",而是"换组织逻辑的方式"。建议路线:先用 setup() 在老组件里桥接,再逐项改写选项区块,接着用 <script setup> 收口,最后把可复用逻辑抽成 composables。过程中牢记 ref 为主、reactive 解构需 toRefs 保活,就能避开大部分响应式陷阱。框架同时兼容两种风格,你完全可以按照自己的节奏逐步推进,不必一步到位。
更多组合式 API 与 <script setup> 实战内容,可前往 CSDN 标签聚合页 查阅相关优质文章。

© 2026 | 转载请注明出处