在 Vue 开发中,你是否遇到过这样的"诡异"现象?
"我明明给对象加了新属性,
console.log
显示它存在,但页面就是不更新!"
"为什么this.obj.newProp = 'value'
不触发视图刷新?"
这背后隐藏着 Vue 响应式系统的根本限制。
本文将从 Object.defineProperty
的缺陷 到 Vue 3 的 Proxy
革命,彻底解析这一经典问题。
一、问题重现:动态添加属性,视图不更新
📌 场景代码
vue
<template>
<div>
<ul>
<li v-for="(value, key) in obj" :key="key">{{ key }}: {{ value }}</li>
</ul>
<button @click="addProp">添加 obj.b</button>
</div>
</template>
<script>
export default {
data() {
return {
obj: {
a: 'obj.a'
}
}
},
methods: {
addProp() {
this.obj.b = 'obj.b'; // ❌ 视图不更新
console.log(this.obj); // ✅ 控制台显示 { a: 'obj.a', b: 'obj.b' }
}
}
}
</script>
🚨 现象
- 控制台:
obj.b
成功添加; - 页面:列表没有新增
b: obj.b
。
二、根本原因:Vue 2 的响应式机制缺陷
✅ Vue 2 响应式原理
Vue 2 使用 Object.defineProperty()
劫持对象属性:
js
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
// 依赖收集
return val;
},
set(newVal) {
val = newVal;
// 派发更新
updateView();
}
});
}
❌ 问题所在
在组件初始化时,Vue 会递归遍历 data
对象的所有属性,并将其转换为响应式。
js
// 初始化时,只处理了 'a'
{
obj: {
a: 'obj.a' // ✅ 被 defineProperty 劫持
// b 不存在,所以不会被劫持
}
}
当你执行:
js
this.obj.b = 'obj.b'; // 直接赋值,未经过 defineProperty
b
属性未被劫持;- 没有
setter
,无法触发updateView()
; - 所以视图不更新。
三、解决方案(Vue 2)
✅ 方案 1:this.$set()
(推荐)
js
addProp() {
this.$set(this.obj, 'b', 'obj.b'); // ✅ 视图更新
}
🔍 this.$set
做了什么?
js
Vue.prototype.$set = function(obj, key, val) {
defineReactive(obj, key, val); // 手动将新属性变为响应式
updateView(); // 触发视图更新
}
💡 本质:手动调用
defineReactive
。
✅ 方案 2:Vue.set()
js
import Vue from 'vue';
addProp() {
Vue.set(this.obj, 'b', 'obj.b'); // ✅ 同 $set
}
✅ 方案 3:使用 Object.assign()
或 展开运算符
js
addProp() {
this.obj = Object.assign({}, this.obj, { b: 'obj.b' });
// 或
this.obj = { ...this.obj, b: 'obj.b' };
}
🔍 原理
- 创建一个新对象;
- Vue 检测到
obj
被重新赋值(引用变化); - 触发整个对象的响应式更新。
⚠️ 缺点:性能稍差,因为是全量更新。
四、Vue 3 的革命性突破:Proxy
✅ Vue 3 响应式原理
Vue 3 使用 Proxy
代理整个对象:
js
const reactive = (obj) => {
return new Proxy(obj, {
get(target, key) {
// 依赖收集
return target[key];
},
set(target, key, val) {
target[key] = val;
// 派发更新
updateView();
return true;
}
});
}
🎉 自动支持动态添加
js
const obj = reactive({ a: 'obj.a' });
obj.b = 'obj.b'; // ✅ 自动触发 setter,视图更新!
💥 Vue 3 中,
this.obj.b = 'value'
可以直接更新视图!
五、其他"陷阱"场景
📌 场景 1:数组索引赋值
js
this.items[0] = 'new'; // ❌ Vue 2 不更新
✅ 解决方案
js
// $set
this.$set(this.items, 0, 'new');
// 或 splice
this.items.splice(0, 1, 'new');
// 或 length
this.items.length = 0; // 清空
📌 场景 2:删除属性
js
delete this.obj.a; // ❌ 不更新
✅ 解决方案
js
this.$delete(this.obj, 'a'); // ✅
六、最佳实践
✅ Vue 2 最佳实践
操作 | 推荐方法 |
---|---|
添加属性 | this.$set(obj, 'key', val) |
删除属性 | this.$delete(obj, 'key') |
更新数组 | splice 、slice 、concat |
✅ Vue 3 最佳实践
- ✅ 直接使用
obj.newKey = value
; - ✅ 直接使用
delete obj.key
; - ⚠️ 仍建议使用
ref
和reactive
的规范用法。
💡 结语
"理解响应式机制,才能避开 Vue 的'坑'。"
版本 | 机制 | 动态添加支持 |
---|---|---|
Vue 2 | Object.defineProperty |
❌ 需 $set |
Vue 3 | Proxy |
✅ 原生支持 |
方法 | 适用场景 |
---|---|
$set |
Vue 2 动态添加属性 |
Object.assign |
简单场景,可接受性能损耗 |
Proxy |
Vue 3,开箱即用 |
记住:
"在 Vue 2 中,不要直接给响应式对象添加属性,否则你会掉进'响应式陷阱'。"