Vue.js 的響應式系統一直是框架的核心。從 Vue 2 到 Vue 3,底層從 Object.defineProperty 切換到 Proxy,不是換個 API 這麼簡單——兩種機制的攔截粒度根本不同,直接決定了哪些操作能被追蹤、哪些不能。
Vue 2:Object.defineProperty 時代
實作方式
Vue 2 在初始化元件時遞迴遍歷整個物件,對每個屬性呼叫 Object.defineProperty,掛上 getter 和 setter:
1 | function defineReactive(obj, key, val) { |
getter/setter 只掛在初始化時已存在的屬性上,這是 Vue 2 所有限制的根源。
優點
- 瀏覽器相容性極佳,IE9 以上皆可使用
- 對現有屬性的讀寫追蹤行為穩定
限制
無法偵測屬性的動態新增或刪除:後來才加進物件的屬性不會有 getter/setter,因此不具響應性。這是為什麼 Vue 2 需要
Vue.set()和this.$set()。1
2
3
4
5// Vue 2:這樣不會觸發更新
this.user.newField = 'hello'
// 必須這樣寫
this.$set(this.user, 'newField', 'hello')陣列的盲區是索引賦值與
length修改:Vue 2 透過覆寫陣列原型(push、pop、splice等變異方法)讓常見操作保有響應性,但直接以索引賦值或改length無法被攔截:1
2
3
4
5
6
7// Vue 2:以下兩種寫法不會觸發更新
this.items[0] = 'x'
this.items.length = 0
// 有效的替代寫法
this.$set(this.items, 0, 'x')
this.items.splice(0)初始化成本隨物件大小線性成長:遞迴遍歷整棵物件樹,大型資料結構在元件建立時就全部處理完。
Vue 3:Proxy 時代
實作方式
Vue 3 改用 Proxy 包裹整個物件,攔截發生在代理物件上的任何操作:
1 | function reactive(target) { |
Proxy 在物件層級攔截,不需要事先知道有哪些屬性。
相較於 Vue 2 的改善
動態屬性不再是問題:因為 set trap 能攔截任何賦值,包含原本不存在的 key,不再需要 Vue.set():
1 | // Vue 3:直接賦值即可觸發更新 |
陣列索引賦值與 length 修改可以被偵測:
1 | // Vue 3:這些現在都能正確觸發更新 |
注意,Vue 3 對 push、pop、includes、indexOf 等方法做了額外的插樁處理,但目的各不相同:push/pop 是為了在變異時正確觸發 trigger;includes/indexOf 的插樁則是為了讓讀取操作也能追蹤依賴——reactive 陣列的元素本身也是 Proxy,直接傳入原生方法比對時會有身份不相等的問題,插樁確保這類讀取也被正確收集。
惰性初始化降低啟動成本:Vue 2 初始化時就遞迴處理整棵物件樹;Vue 3 改為惰性建立,只在屬性真正被存取時才對嵌套物件建立 Proxy。大型資料結構的元件啟動成本因此顯著降低。需要說明的是,單次屬性存取的速度 Proxy 本身並不比 defineProperty 快,某些 V8 版本甚至略慢——整體效能優勢來自「不必事先全部初始化」的設計,而非 Proxy 本身更快。
in 運算子與 for...in 也納入追蹤:透過 has 和 ownKeys trap,key in obj 或 Object.keys(obj) 這類操作也可以建立依賴關係,Vue 2 做不到。
為什麼 Vue 2 的方式撐到了 2.x 才換?
defineProperty 在 2014 年 Vue 1.0 問世時已是最佳選項——Proxy 雖然 ES2015 就定義了,但 IE11 不支援且無法 polyfill。Vue 2 選擇 defineProperty 是為了兼顧當時的瀏覽器生態。
Vue 3 在 2020 年正式發布,明確放棄 IE11 支援。IE11 已於 2022 年 6 月 15 日正式終止支援,現代專案無需再考慮此限制。如果你的專案確實需要跑在 IE11 上,唯一選項是留在 Vue 2(Vue 2 已於 2023 年 12 月 31 日終止官方支援,但仍可使用)。
遷移時實際需要注意什麼
從 Vue 2 升上 Vue 3,響應式系統的行為差異主要體現在:
- 移除
Vue.set()/this.$set():直接賦值即可,不再需要 reactive()回傳的是 Proxy,直接把它解構或傳給需要原始物件的第三方程式庫時會失去響應性;用toRefs()或storeToRefs()保留
1 | // 解構後失去響應性(常見陷阱) |
$delete同樣不再需要,直接delete obj.key即可觸發更新




