开发者问题收集

对数组内对象的更新不会触发更新

2019-09-06
3287

在我的根 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 });

我还尝试使用深度观察器,如类似问题中所建议的那样,但它在我的情况下不起作用。当我使用深度观察器时,函数 调用,但无论我将哪个值设置为我的属性, newParamsoldParams 始终是同一个对象。

有没有解决方案可以让我仅通过设置属性就可以改变数组项?那将是最理想的结果。

2个回答

首先要做的事情

使用 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>

关于此的一些说明:

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

这种方法的主要优势在于处理复制的位置。执行变异的代码不需要担心它,复制是由需要复制的相同组件执行的。

skirtle
2019-09-06

正如您所说,您将需要在 watch 中使用 deep 属性。

使用 Vue.set 您应该重新安装数组内的整个对象,例如:

const newObj = {
  name: 'test1',
  params: {
    testParam: 1,
  },
};

Vue.set(yourArray, newObj, yourIndex);

请注意,您正在数组内设置一些值,在这种情况下,数组包含对象。

Matheus Valenza
2019-09-06