学完这篇你能拿到一份可以直接对照着改的 Options API → Composition API 迁移差异清单,知道每一类改动怎么写、哪里最容易翻车。下面按迁移顺序拆成七步,改了就能跑。
第一步:先用迁移构建跑一遍,别急着全量重写
Vue 3 提供了 @vue/compat 迁移构建,它会让 Vue 3 以 Vue 2 的行为运行,并在控制台打出兼容性警告。做法是在构建配置里把 vue 别名指向 @vue/compat:
// vue.config.js
module.exports = {
chainWebpack: config => {
config.resolve.alias.set('vue', '@vue/compat')
config.module.rule('vue').use('vue-loader').tap(options => ({
...options,
compilerOptions: { compatConfig: { MODE: 2 } }
}))
}
}
启动后按控制台警告逐条改,把 MODE 从 2 调到 3,警告清零就迁移完了。
注意:迁移构建只是过渡工具,不要留在生产环境长期使用,它体积更大、性能更差,改完就该移除。
第二步:data 变成 ref / reactive
// Vue 2
data() { return { count: 0, form: { name: '' } } }
// Vue 3
import { ref, reactive, toRefs } from 'vue'
const count = ref(0)
const form = reactive({ name: '' })
三个高频坑:
ref 在 JS 里必须 .value,在模板里自动解包,别在模板里也写 .value。
- 直接解构
reactive 对象会丢掉响应式,要用 const { name } = toRefs(form)。
- 想把
reactive 整体替换(比如接口返回新对象),用 Object.assign(form, newData),或者干脆用 ref({}) 包一层。
注意:Vue 2 里「数组下标赋值、对象新增属性不响应」的老毛病在 Vue 3 的 Proxy 下已经不存在,this.$set 不用再迁移,直接删。
第三步:computed、watch、nextTick 全部变成函数
const total = computed(() => price.value * count.value)
const keyword = computed({
get: () => query.value,
set: v => { query.value = v }
})
watch(count, (n, o) => { /* ... */ }, { immediate: true })
关键差异:
watch 的 deep 默认是 false。监听 ref 包裹的对象时必须显式写 deep: true;监听 reactive 对象则默认就是深度监听。
this.$nextTick 改成 import { nextTick } from 'vue',调用 nextTick(() => {...})。
- 依赖多个来源、不需要旧值时,用
watchEffect 更省事,但它自动收集依赖,别在里面写副作用太重的逻辑。
第四步:生命周期改名,this 消失
| Vue 2 | Vue 3 |
|---|
| beforeCreate / created | 直接在 setup 顶层写 |
| mounted | onMounted |
| beforeDestroy | onBeforeUnmount |
| destroyed | onUnmounted |
| errorCaptured | onErrorCaptured |
这一步最大的问题不是改名,而是 this 没了。this.$router 换成 useRouter() / useRoute(),this.$store 换成 useStore(),this.$refs.xx 换成 const el = ref(null) 配合模板里的 ref="el"。
注意:onMounted 里才能拿到真实 DOM,setup 顶层执行时组件还没挂载。另外确实要拿组件实例时用 getCurrentInstance(),但它是内部 API,能用组合式函数就别用它。
第五步:props / emits / v-model / 事件总线
<script setup> 下用编译宏,不需要 import:
const props = defineProps({ title: String })
const emit = defineEmits(['update', 'close'])
- v-model 默认 prop 从
value 变成 modelValue,事件从 input 变成 update:modelValue;自定义 model: { prop, event } 改写成 v-model:propName。
.sync 被移除,v-bind:title.sync="t" 改写成 v-model:title="t"。
$listeners 被合并进 $attrs,原来靠它透传的事件不会被自动继承,inheritAttrs: false 的写法仍然有效。
$on / $off / $once 完全移除,事件总线要么换 mitt,要么改用 provide / inject,要么老老实实走 props + emit。
第六步:全局 API、filters、指令、mixin
Vue.use(...) → app.use(...);Vue.prototype.$http → app.config.globalProperties.$http。但 setup 里拿不到 this,更推荐 provide / inject 或直接 import。
filters 被移除,改写成 computed 或普通函数,在模板里当函数调用。
- 自定义指令钩子改名:
bind → beforeMount,inserted → mounted,update → updated,unbind → unmounted。另外 updated 的触发时机比 Vue 2 的 update 更晚,原来靠它做 DOM 同步的逻辑要重新验证。
mixin 建议整体换成组合式函数 useXxx(),逻辑来源清晰、不会命名冲突。
第七步:验收清单
改完逐条打勾:控制台无兼容警告、所有 this. 已清零、ref 的 .value 没漏没多、reactive 解构已用 toRefs、watch 的 deep 按需补齐、事件总线已替换、过滤器已清除、指令钩子已改名。
小结
- 先用
@vue/compat 迁移构建跑警告清单,按警告改比人肉比对快得多。
data → ref/reactive,注意 .value、toRefs 和整体替换三个坑。
- 计算、监听、nextTick 都变成显式导入的函数,
watch 的 deep 默认值变了。
- 生命周期改名,
this 消失,路由、store、ref 都改用组合式 API 获取。
- v-model 换 prop 名、
.sync 移除、事件总线 API 删除,这三处最容易漏改。
mixin 全面转向 useXxx() 组合式函数。