Vue 2 中响应式失效的常见情况
Vue 2 使用 Object.defineProperty 实现响应式,这带来了一些局限性。以下是导致响应式失效的详细情况:
1. 对象属性的添加/删除
情况:动态添加新属性
javascript
data() {
return {
user: {
name: '张三',
age: 25
}
};
},
methods: {
addProperty() {
// 错误:直接添加新属性,Vue 无法检测
this.user.gender = '男'; // ❌ 非响应式
// 正确:使用 Vue.set 或 $set
this.$set(this.user, 'gender', '男'); // ✅
},
deleteProperty() {
// 错误:直接删除属性
delete this.user.age; // ❌
// 正确:使用 Vue.delete 或 $delete
this.$delete(this.user, 'age'); // ✅
}
}
2. 数组索引操作
情况:通过索引直接设置数组项
javascript
data() {
return {
list: ['a', 'b', 'c']
};
},
methods: {
updateArray() {
// 错误:通过索引直接修改
this.list[1] = 'x'; // ❌ 非响应式
// 正确方法1:使用 Vue.set
this.$set(this.list, 1, 'x'); // ✅
// 正确方法2:使用数组的变异方法
this.list.splice(1, 1, 'x'); // ✅
}
}
注意:通过索引修改数组中某个元素的属性不会响应式失效(对数组中的元素动态新增或删除属性会响应式失效)
上面情况对于for of循环和foreach也适用:修改元素的属性不会导致响应式失效,直接修改数组中的元素会导致响应式失效
Vue 2 无法检测到通过索引直接设置数组元素的变化
javascript
export default {
data() {
return {
items: ['A', 'B', 'C', 'D']
};
},
methods: {
testForOfReassignment() {
// ❌ 在 for...of 中重新赋值元素 - 完全无效
for (let item of this.items) {
item = 'X'; // 这只是修改了局部变量 item
}
console.log(this.items); // 仍然是 ['A', 'B', 'C', 'D']
// 原数组没有任何变化!
}
}
};
为什么会这样?
javascript
// 理解 for...of 的本质
const arr = ['A', 'B', 'C'];
// for...of 循环实际上是这样工作的:
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
const item = result.value; // item 是值的拷贝
// 1. 如果是基本类型(字符串、数字),是完全的拷贝
// 2. 如果是对象,是引用的拷贝
item = 'X'; // 只是修改了局部变量 item
// 对原数组 arr 没有任何影响!
result = iterator.next();
}
正确的循环修改方法
javascript
methods: {
// ✅ 方法1:使用 map 创建新数组
updateWithMap() {
this.numbers = this.numbers.map(item => item * 2);
},
// ✅ 方法2:使用 splice 修改
updateWithSplice() {
for (let i = 0; i < this.numbers.length; i++) {
this.numbers.splice(i, 1, this.numbers[i] * 2);
}
},
// ✅ 方法3:使用 $set
updateWithSet() {
for (let i = 0; i < this.numbers.length; i++) {
this.$set(this.numbers, i, this.numbers[i] * 2);
}
},
// ✅ 方法4:使用 for...of 配合 $set
updateForOfWithSet() {
let index = 0;
for (const item of this.numbers) {
this.$set(this.numbers, index, item * 2);
index++;
}
}
}
题外话:splice 用法
使用splice 是能够使数组保持响应式的,下面讲一下splice的基本用法
splice 的基本语法
javascript
array.splice(start, deleteCount, item1, item2, ...)
// start: 开始修改的索引
// deleteCount: 要删除的元素数量(可选,默认0)
// item1, item2...: 要添加的元素(可选)
splice 的各种用法
javascript
const fruits = ['苹果', '香蕉', '橙子', '葡萄'];
// 1. 删除元素
fruits.splice(1, 1); // 从索引1开始删除1个元素
// fruits变为: ['苹果', '橙子', '葡萄']
// 2. 删除并替换
fruits.splice(1, 2, '芒果', '草莓');
// 从索引1开始删除2个元素,然后插入'芒果'和'草莓'
// fruits变为: ['苹果', '芒果', '草莓', '葡萄']
// 3. 只添加不删除
fruits.splice(2, 0, '菠萝', '西瓜');
// 从索引2开始,删除0个元素,添加'菠萝'和'西瓜'
// fruits变为: ['苹果', '芒果', '菠萝', '西瓜', '草莓', '葡萄']
// 4. 删除到最后
fruits.splice(3); // 从索引3开始删除到末尾
// fruits变为: ['苹果', '芒果', '菠萝']
// 5. 获取被删除的元素
const removed = fruits.splice(1, 2);
console.log(removed); // ['芒果', '菠萝']
console.log(fruits); // ['苹果', '西瓜', '草莓', '葡萄']
3. 修改数组长度
情况:直接修改数组 length
javascript
data() {
return {
items: ['a', 'b', 'c', 'd']
};
},
methods: {
changeLength() {
// 错误:直接修改 length
this.items.length = 2; // ❌ 非响应式
// 正确:使用 splice
this.items.splice(2); // ✅ 移除索引2之后的所有元素
}
}
4. 初始化时未声明的属性
情况:data 中未声明,后续添加
javascript
export default {
data() {
return {
// 只声明了 name,没有声明 age
person: {
name: '张三'
}
};
},
created() {
// 错误:添加未声明的属性
this.person.age = 25; // ❌ 非响应式
// 正确:在 data 中预先声明
// data() { return { person: { name: '', age: null } }; }
// 或使用 $set
this.$set(this.person, 'age', 25); // ✅
}
};
5. 使用 Object.freeze()
情况:冻结对象
javascript
data() {
return {
// 冻结的对象无法被修改
frozenData: Object.freeze({
title: '固定标题',
items: ['a', 'b']
})
};
},
methods: {
updateFrozen() {
// 这些都不会生效
this.frozenData.title = '新标题'; // ❌
this.frozenData.items.push('c'); // ❌
}
}
6. 使用索引直接访问嵌套数组
情况:嵌套数组的多层索引操作
javascript
data() {
return {
matrix: [
[1, 2],
[3, 4]
]
};
},
methods: {
updateNestedArray() {
// 错误:多层索引直接赋值
this.matrix[0][1] = 99; // ❌ 非响应式
// 正确:使用 $set 或变异方法
this.$set(this.matrix[0], 1, 99); // ✅
// 或者重新赋值整行
const newRow = [...this.matrix[0]];
newRow[1] = 99;
this.matrix.splice(0, 1, newRow); // ✅
}
}
7. 计算属性中的副作用操作
情况:在计算属性中修改数据
javascript
data() {
return {
count: 0
};
},
computed: {
// 错误:计算属性中不应该有副作用
badComputed() {
this.count++; // ❌ 永远不要这样做!
return this.count * 2;
}
}
8. 在 beforeCreate 中修改数据
情况:生命周期过早
javascript
export default {
data() {
return {
message: 'hello'
};
},
beforeCreate() {
// 错误:此时 data 还未初始化
this.message = 'world'; // ❌ 会报错或无效
},
created() {
// 正确:在 created 中修改
this.message = 'world'; // ✅
}
};
9. 直接引用外部对象
情况:引用外部非响应式对象
javascript
const externalData = { value: 1 };
export default {
data() {
return {
// 错误:直接引用外部对象
internalData: externalData, // ❌ 非响应式
// 正确:创建新对象或深拷贝
internalData: { ...externalData } // ✅
};
}
};
10. 使用 JSON.parse() 创建的对象
情况:解析 JSON 字符串
javascript
methods: {
loadFromJSON() {
const jsonString = '{"name":"张三","info":{"age":25}}';
// 错误:直接使用 parse 结果
this.user = JSON.parse(jsonString); // ✅ 顶层的 user 是响应式的
// 但是后续添加属性可能有问题
// 后续操作
this.user.info.gender = '男'; // ❌ 嵌套对象可能需要 $set
}
}
11. 原型链上的属性
情况:访问原型链属性
javascript
data() {
return {
obj: Object.create({
protoProperty: '原型属性'
})
};
},
methods: {
accessProto() {
// Vue 无法检测原型链上的属性变化
console.log(this.obj.protoProperty); // 可以访问
// 但修改可能不会触发更新
}
}