Vue2無法監(jiān)聽數(shù)組下標(biāo)和對象新增屬性的原因和解決方法
現(xiàn)象回顧:那些讓人頭禿的 Bug
在 Vue 2 開發(fā)中,你一定遇到過以下場景:
? 場景 1:直接通過索引修改數(shù)組
data() {
return {
list: ['a', 'b', 'c']
}
},
methods: {
updateItem() {
this.list[0] = 'x'; // ? 視圖不會更新!
console.log(this.list); // ['x', 'b', 'c'] (數(shù)據(jù)變了,但界面沒變)
}
}
? 場景 2:直接添加新屬性
data() {
return {
user: { name: 'Alice' }
}
},
methods: {
addAge() {
this.user.age = 25; // ? 視圖不會更新!
console.log(this.user); // { name: 'Alice', age: 25 } (數(shù)據(jù)變了,但界面沒變)
}
}
疑問:既然 JavaScript 對象和數(shù)組都是引用類型,為什么 Vue 能監(jiān)聽到 push 或普通屬性的修改,卻對“下標(biāo)賦值”和“新增屬性”視而不見?
答案藏在 Vue 2 的核心實現(xiàn)—— Object.defineProperty 中。
1. 核心原理:Object.defineProperty 的工作方式
Vue 2 在初始化時,會遍歷 data 中的所有屬性,并使用 Object.defineProperty 為它們定義 getter 和 setter。
// 簡化版 Vue 2 響應(yīng)式初始化
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`讀取了 ${key}`);
// 收集依賴(Dep)
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
console.log(`設(shè)置了 ${key} 為 ${newVal}`);
// 通知更新(Notify)
},
});
}
關(guān)鍵點:Object.defineProperty 是針對特定屬性進行劫持的。它只能監(jiān)聽已經(jīng)存在于對象上的屬性。
2. 痛點一:為什么不能監(jiān)聽對象屬性的新增/刪除?
原因分析
當(dāng)你執(zhí)行 this.user.age = 25 時:
user對象在初始化時只有name屬性。- Vue 只為
name定義了getter/setter。 age是一個全新的屬性,它身上沒有任何getter/setter。- JavaScript 引擎直接將該屬性添加到對象上,完全繞過了 Vue 的攔截機制。
- Vue 根本不知道
age被添加了,因此不會觸發(fā)視圖更新。
同理,delete this.user.name 只是刪除了屬性,也不會觸發(fā)任何通知。
比喻:
Vue 2 像一個保安,只認(rèn)識門口登記過的住戶(已定義的屬性)。
如果一個陌生人(新屬性)直接翻 墻進來(直接賦值),保安根本看不見,也不會通知業(yè)主(視圖)。
解決方案:Vue.set/this.$set
Vue 提供了 API 來手動觸發(fā)響應(yīng)式:
// 語法:Vue.set(target, propertyName/index, value) this.$set(this.user, "age", 25);
內(nèi)部原理:
- 判斷目標(biāo)是否是響應(yīng)式對象。
- 如果是新屬性,調(diào)用
Object.defineProperty為該屬性添加getter/setter。 - 手動觸發(fā)依賴通知。
3. 痛點二:為什么不能監(jiān)聽數(shù)組下標(biāo)的變化?
原因分析
當(dāng)你執(zhí)行 this.list[0] = 'x' 時:
- 數(shù)組在初始化時,Vue 會遍歷其元素。如果元素是對象,會遞歸劫持;如果是基本類型,數(shù)組本身并沒有為每個索引(0, 1, 2…)定義
getter/setter。 - 性能考量:如果為數(shù)組的每一個索引都定義
getter/setter,當(dāng)數(shù)組長度為 10,000 時,內(nèi)存開銷巨大,且初始化極慢。 - 語言限制:
Object.defineProperty雖然可以監(jiān)聽索引,但 Vue 2 出于性能考慮,沒有對數(shù)組索引進行劫持。 - 因此,直接通過索引賦值
list[0] = 'x'只是一個普通的 JavaScript 賦值操作,不會觸發(fā)setter。
注意:你可能聽說過“Vue 重寫了數(shù)組方法”。是的,Vue 重寫了 push, pop, shift, unshift, splice, sort, reverse 這 7 個方法。
- 這些方法會改變數(shù)組長度或內(nèi)容,Vue 在這些方法內(nèi)部手動觸發(fā)了通知。
- 但是,
list[0] = 'x'不是方法調(diào)用,而是屬性賦值,所以無法被攔截。
解決方案
使用變異方法:
this.list.splice(0, 1, "x"); // ? 觸發(fā)更新
使用 Vue.set:
this.$set(this.list, 0, "x"); // ? 觸發(fā)更新
替換整個數(shù)組:
this.list = [...this.list]; // 或者使用 slice, concat 返回新數(shù)組 // 賦值給 this.list 會觸發(fā) list 屬性的 setter,從而更新視圖
4. 解決方案:Vue 2 是如何“打補丁”的?
為了彌補 Object.defineProperty 的缺陷,Vue 2 做了兩件事:
1. 重寫數(shù)組原型方法
Vue 攔截了數(shù)組的 7 個變異方法,在執(zhí)行原生方法后,手動調(diào)用 ob.dep.notify() 通知更新。
// 偽代碼
const arrayProto = Array.prototype;
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse",
];
methodsToPatch.forEach((method) => {
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
const result = original.apply(this, args);
const ob = this.__ob__;
ob.dep.notify(); // 手動通知
return result;
});
});
2. 提供$set和$deleteAPI
允許開發(fā)者手動將新屬性轉(zhuǎn)化為響應(yīng)式,或刪除響應(yīng)式屬性并通知更新。
5. 對比 Vue 3:Proxy 如何完美解決?
Vue 3 使用 Proxy 替代了 Object.defineProperty,徹底解決了上述問題。
Proxy 的優(yōu)勢
- 攔截整個對象:
Proxy代理的是整個對象,而不是單個屬性。 - 攔截所有操作:包括屬性的讀取、賦值、刪除、甚至
in操作符。 - 天然支持?jǐn)?shù)組索引:對數(shù)組索引的賦值會被
set陷阱捕獲。 - 天然支持新增屬性:對新屬性的賦值也會被
set陷阱捕獲。
// Vue 3 簡化原理
const data = new Proxy(
{},
{
get(target, key) {
track(target, key); // 收集依賴
return Reflect.get(target, key);
},
set(target, key, value) {
const result = Reflect.set(target, key, value);
trigger(target, key); // 觸發(fā)更新
return result;
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key);
trigger(target, key); // 觸發(fā)更新
return result;
},
},
);
// ? 以下操作都能被攔截并觸發(fā)更新
data.list[0] = "x";
data.user.age = 25;
delete data.user.name;
結(jié)論:Vue 3 不再需要 $set,也不再需要擔(dān)心數(shù)組下標(biāo)的問題。代碼更符合 JavaScript 原生直覺。
6. 總結(jié)
| 特性 | Vue 2 (Object.defineProperty) | Vue 3 (Proxy) |
|---|---|---|
| 監(jiān)聽機制 | 遞歸定義屬性的 getter/setter | 代理整個對象,攔截所有操作 |
| 對象新增屬性 | ? 無法監(jiān)聽(需 $set) | ? 原生支持 |
| 對象刪除屬性 | ? 無法監(jiān)聽(需 $delete) | ? 原生支持 |
| 數(shù)組索引賦值 | ? 無法監(jiān)聽(需 splice 或 $set) | ? 原生支持 |
| 數(shù)組長度修改 | ? 無法監(jiān)聽 | ? 原生支持 |
| 性能 | 初始化慢(遞歸遍歷) | 初始化快(懶代理) |
?? 博主寄語:
理解 Vue 2 的局限性,不僅能幫你避免 Bug,更能讓你深刻體會技術(shù)演進的必要性。Object.defineProperty 是時代的產(chǎn)物,而 Proxy 則是現(xiàn)代化的利器。
記住口訣:
Vue 2 劫持靠定義,
新增下標(biāo)難留意。
若要更新需 Set,
變異方法也可以。
Vue 3 代理更強大,
任意操作全攔截。
代碼直觀無死角,
響應(yīng)系統(tǒng)真厲害。
希望這篇文檔能幫你徹底搞懂 Vue 2 響應(yīng)式的底層原理!
以上就是Vue2無法監(jiān)聽數(shù)組下標(biāo)和對象新增屬性的原因和解決方法的詳細(xì)內(nèi)容,更多關(guān)于Vue2無法監(jiān)聽數(shù)組下標(biāo)和對象新增屬性的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue2引入Three.js預(yù)覽3D文件的實現(xiàn)方法
three.js是一個基于WebGL的JavaScript庫,它允許開發(fā)者在網(wǎng)頁上創(chuàng)建和顯示3D圖形,這篇文章主要介紹了vue2引入Three.js預(yù)覽3D文件的實現(xiàn)方法,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2025-10-10
vue使用canvas畫布實現(xiàn)平面圖點位標(biāo)注功能(最新推薦)
這篇文章主要介紹了vue使用canvas畫布實現(xiàn)平面圖點位標(biāo)注功能,經(jīng)過本文一番研究發(fā)現(xiàn)canvas標(biāo)簽可以完成很多功能,電子簽名,點位標(biāo)注,問題標(biāo)注,畫圖功能,感興趣的朋友跟隨小編一起看看吧2023-07-07
vue填坑之webpack run build 靜態(tài)資源找不到的解決方法
今天小編就為大家分享一篇vue填坑之webpack run build 靜態(tài)資源找不到的解決方法。具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09
Vue.js中使用Vuex但未正確配置Store問題以及解決過程
這篇文章主要介紹了Vue.js中使用Vuex但未正確配置Store問題以及解決過程,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-05-05

