從 Vue 2 遷移到 Vue 3,最容易出問題的不是語法本身,而是「預設行為改變」:同樣的程式碼在新版仍能跑,但行為不一樣。這篇整理元件層級最常碰到的幾個差異,以及遷移時的檢查重點。
響應式系統:defineProperty 到 Proxy
Vue 2 用 Object.defineProperty 攔截屬性,所以有幾個限制:新增屬性、用索引改陣列元素,都偵測不到,必須使用 Vue.set。Vue 3 改用 Proxy,這些限制消失了。
js
// Vue 2:新增屬性不會觸發更新
this.user.age = 30 // 無響應
this.$set(this.user, 'age', 30)
// Vue 3:直接指定即可
state.user.age = 30
但 Proxy 帶來另一個陷阱:解構會丟失響應性。
js
import { reactive, toRefs } from 'vue'
const state = reactive({ count: 0 })
const { count } = state // count 只是一個普通數字,不再響應
const { count: c } = toRefs(state) // c 是 ref,保持響應
生命週期對照
| Vue 2 | Vue 3(Options API) | Composition API |
|---|---|---|
beforeCreate / created | 同左 | 直接寫在 setup() 內 |
beforeMount / mounted | 同左 | onBeforeMount / onMounted |
beforeUpdate / updated | 同左 | onBeforeUpdate / onUpdated |
beforeDestroy | beforeUnmount | onBeforeUnmount |
destroyed | unmounted | onUnmounted |
重點是 destroy 改名為 unmount。遷移時如果漏改,舊名稱的鉤子在 Vue 3 不會執行,原本放在裡面的清理工作(移除監聽、清除計時器)就默默失效了。
v-model 的改變
Vue 2 的 v-model 預設綁 value prop 與 input 事件;Vue 3 改成 modelValue 與 update:modelValue,並且支援多個 v-model。
vue
<!-- 父層 -->
<UserForm v-model:name="name" v-model:email="email" />
vue
<!-- 子層(Vue 3.4+):defineModel 取代手動宣告 props 與 emit -->
<script setup>
const name = defineModel('name')
const email = defineModel('email')
</script>
<template>
<input v-model="name">
<input v-model="email">
</template>
自訂元件如果在 Vue 2 依賴 value / input,遷移時要改成 modelValue / update:modelValue,否則父層的 v-model 會失效。
插槽與事件 API
- Vue 3 把
$scopedSlots併入$slots,所有插槽都是函式;範本語法統一用v-slot或#。 $on、$off、$once在 Vue 3 被移除。以往拿元件實例當 Event Bus 的寫法需要改用mitt之類的函式庫,或改成 props / emits、provide/inject。- 元件可以有多個根節點(Fragments)。這時
class等 fallthrough 屬性不再自動套用到單一根節點,需要用v-bind="$attrs"指定位置。
遷移檢查清單
- 找出所有
value/input風格的自訂v-model,改成modelValue。 - 搜尋
beforeDestroy、destroyed、$on、$off、$scopedSlots、$set。 - 檢查所有解構
reactive物件的地方,改用toRefs或直接存取屬性。 - 多根節點元件:確認
$attrs套用在預期的元素上。 - 使用 UI 元件庫時,同步查閱該版本的遷移指南,因為元件 API 與預設樣式常有差異。
小結
遷移的核心是「理解差異再改」,而不是把舊版寫法原封不動搬到新版。生命週期改名、v-model 協議、解構失去響應性這三項,最容易在沒有錯誤訊息的情況下造成行為異常,值得優先逐項檢查。