首页 > 解决方案 > 更新数组内的对象不会触发更新

问题描述

在我的根 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.js

解决方案


第一件事。

使用Vue.set不会有帮助。Vue.set用于设置 Vue 的响应式系统无法跟踪的属性的值。这包括按索引更新数组或向对象添加新属性,但这些都不适用。您正在更新反应对象的现有属性,因此 usingVue.set除了使用=.

下一个...

Vue 在将对象作为道具传递时不会复制它们。如果您将对象作为道具传递,则子组件将获得与父组件相同的对象的引用。如果deep您更新该对象中的属性但它仍然是同一个对象,则会触发观察者。传递给观察者的旧值和新值将是同一个对象。这在文档中有所说明:

https://vuejs.org/v2/api/#vm-watch

注意:当改变(而不是替换)对象或数组时,旧值将与新值相同,因为它们引用相同的对象/数组。Vue 不保留 pre-mutate 值的副本。

正如您所注意到的,一种解决方案是在执行更新时使用全新的对象。最终,如果您想比较新旧对象,那么您别无选择,只能在某处复制该对象。变异时复制是一个完全有效的选择,但这不是唯一的选择。

另一种选择是使用计算属性来创建副本:

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/vue@2.6.10/dist/vue.js"></script>
<div id="app">
  <input v-model="params.name">
  <input v-model="params.id">
  <input v-model="params.age">
</div>

对此有几点说明:

  1. 此示例中的计算属性仅创建一个浅拷贝。如果你需要一个深拷贝,它会更复杂,比如JSON.stringify/JSON.parse可能是一个选项。
  2. 计算属性实际上不必复制所有内容。如果您只想查看属性的子集,则只需复制这些属性。
  3. watch不需要是deep。计算属性将创建对其使用的属性的依赖关系,如果其中任何一个发生更改,它将被重新计算,每次都创建一个新对象。我们只需要观察那个物体。
  4. Vue 缓存计算属性的值。当依赖项更改时,旧值被标记为过时但不会立即丢弃,以便可以将其传递给观察者。

这种方法的主要优点是处理复制的地方。进行变异的代码不需要担心它,复制是由需要复制的同一组件执行的。


推荐阅读