学完这篇你能拿到一份可以直接对照着改的 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()` 组合式函数。