对数组内对象的更新不会触发更新
在我的根 Vue 实例中,我有一个包含一些数据的对象数组,我使用它来渲染一组组件。这些组件在提供给它们的数据对象上有一个观察器,每次对象更新时都会进行异步调用。
问题是,当我更新数组中某个对象的属性时,不会调用观察器。它不应该落入 Vue 的任何警告,因为 a) 我没有添加新属性,只是更新现有属性,并且 b) 我没有以任何方式改变数组本身。那么为什么会发生这种情况呢?我该如何修复它呢?
我的主要 Vue 实例:
let content = new Vue({
el: '#content',
data: {
testData: [
{ name: 'test1', params: {
testParam: 1
} },
{ name: 'test2', params: {
testParam: 1
} },
{ name: 'test3', params: {
testParam: 1
} }
]
}
});
我用来呈现组件的代码:
<div id="content">
<div v-for="item in testData">
<test-component v-bind="item"></test-component>
</div>
</div>
还有我的组件:
Vue.component('test-component', {
props: {
name: {
type: String,
required: true
},
params: {
type: Object,
required: true
}
},
data: function() {
return { asyncResult: 0 };
},
watch: {
params: function(newParams, oldParams) {
// I use a custom function to compare objects, but that's not the issue since it isn't even being called.
console.log(newParams);
if(!this.compareObjs(newParams, oldParams)) {
// My async call, which mutates asyncResult
}
}
},
template: `
<span>{{ asyncResult }}</span>
`
});
我的目标是改变给定对象的
params
属性的属性并触发观察器以重新呈现相应的组件,但是当我尝试直接改变它时,它不起作用。
示例(以及我希望我的组件工作的方式):
content.testData[2].params.testParam = 5;
不幸的是,它没有。使用
Vue.set
也不起作用:
Vue.set(content.testData[2].params, 'testParam', 5);
我发现唯一有效的方法是完全分配一个新对象(每次我必须改变属性时我都不想这样做):
content.testData[2].params = Object.assign({}, content.testData[2].params, { testParam: 5 });
我还尝试使用深度观察器,如类似问题中所建议的那样,但它在我的情况下不起作用。当我使用深度观察器时,函数
被
调用,但无论我将哪个值设置为我的属性,
newParams
和
oldParams
始终是同一个对象。
有没有解决方案可以让我仅通过设置属性就可以改变数组项?那将是最理想的结果。
首先要做的事情
使用
Vue.set
没有帮助。
Vue.set
用于设置 Vue 的反应系统无法跟踪的属性值。这包括通过索引更新数组或向对象添加新属性,但这些都不适用于此处。您正在更新反应对象的现有属性,因此使用
Vue.set
除了使用
=
设置它之外不会执行任何其他操作。
下一步...
在将对象作为 props 传递时,Vue 不会复制对象。如果将对象作为 prop 传递,则子组件将获得与父组件相同的对象的引用。如果您更新该对象内的属性但它仍然是同一个对象,则会触发
deep
观察程序。传递给观察程序的旧值和新值将是同一个对象。文档中提到了这一点:
https://v2.vuejs.org/v2/api/#vm-watch
Note: when mutating (rather than replacing) an Object or an Array, the old value will be the same as new value because they reference the same Object/Array. Vue doesn’t keep a copy of the pre-mutate value.
正如您所注意到的,一种解决方案是在执行更新时使用一个全新的对象。最终,如果您想比较新旧对象,那么您别无选择,只能在 某处 复制该对象。在变异时复制副本是一种完全有效的选择,但这不是唯一的选择。
另一种选择是使用计算属性来创建副本:
new Vue({
el: '#app',
data () {
return {
params: {
name: 'Lisa',
id: 5,
age: 27
}
}
},
computed: {
watchableParams () {
return {...this.params}
}
},
watch: {
watchableParams (newParams, oldParams) {
console.log(newParams, oldParams)
}
}
})
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>
<div id="app">
<input v-model="params.name">
<input v-model="params.id">
<input v-model="params.age">
</div>
关于此的一些说明:
-
此示例中的计算属性仅创建浅表副本。如果您需要深度复制,则可能会更复杂,
JSON.stringify
/JSON.parse
之类的东西可能是一个选项。 - 计算属性实际上不必复制所有内容。如果您只想监视属性的子集,则只需复制这些子集即可。
-
watch
不需要深度
。计算属性将在其使用的属性上创建依赖项,如果其中任何一个发生更改,它将被重新计算,每次都会创建一个新对象。我们只需要监视该对象。 - Vue 缓存计算属性的值。当依赖项更改时,旧值将被标记为过时,但不会立即丢弃,以便可以将其传递给观察者。
这种方法的主要优势在于处理复制的位置。执行变异的代码不需要担心它,复制是由需要复制的相同组件执行的。
正如您所说,您将需要在
watch
中使用
deep
属性。
使用
Vue.set
您应该重新安装数组内的整个对象,例如:
const newObj = {
name: 'test1',
params: {
testParam: 1,
},
};
Vue.set(yourArray, newObj, yourIndex);
请注意,您正在数组内设置一些值,在这种情况下,数组包含对象。