Vue - 监听对象数组的变化并计算变化内容?

139

我有一个名为people的数组,包含如下对象:

Before

[
  {id: 0, name: 'Bob', age: 27},
  {id: 1, name: 'Frank', age: 32},
  {id: 2, name: 'Joe', age: 38}
]

它可以改变:

之后

[
  {id: 0, name: 'Bob', age: 27},
  {id: 1, name: 'Frank', age: 33},
  {id: 2, name: 'Joe', age: 38}
]

注意,弗兰克刚刚过了33岁生日。

我有一个应用程序,我正在尝试监视人员数组,当任何值更改时记录更改:


<style>
input {
  display: block;
}
</style>

<div id="app">
  <input type="text" v-for="(person, index) in people" v-model="people[index].age" />
</div>

<script>
new Vue({
  el: '#app',
  data: {
    people: [
      {id: 0, name: 'Bob', age: 27},
      {id: 1, name: 'Frank', age: 32},
      {id: 2, name: 'Joe', age: 38}
    ]
  },
  watch: {
    people: {
      handler: function (val, oldVal) {
        // Return the object that changed
        var changed = val.filter( function( p, idx ) {
          return Object.keys(p).some( function( prop ) {
            return p[prop] !== oldVal[idx][prop];
          })
        })
        // Log it
        console.log(changed)
      },
      deep: true
    }
  }
})
</script>

我基于我昨天提出的关于比较数组和选择最快的答案的问题(链接),来完成这个任务。

所以,到了这一步,我期望看到以下结果:{ id: 1, name: 'Frank', age: 33 }

但是在控制台中输出的是(需要注意的是,我把它放在组件中):

[Vue warn]: Error in watcher "people" 
(found in anonymous component - use the "name" option for better debugging messages.)

在我制作的CodePen中,结果是一个空数组而不是我预期的已更改的对象。

如果有人能够建议为什么会发生这种情况或者我哪里做错了,那将不胜感激,非常感谢!


我对下面所有解决方案的唯一问题是它们在每次更改时都会触发,而我想要的是仅对更改的项进行数据库 API 调用。一个输入 v-model 会触发数百次,我想记录更改的发生,但只有当用户点击应用更改时才将它们保存到数据库中,你会如何处理呢? - PirateApp
1
@PirateApp 不太确定我是否完全理解了你的问题,但也许这可以解决你遇到的问题:https://vuejs.org/v2/guide/forms.html#lazy。你也可以选择通过使用 select / change 事件来触发你的逻辑来更深入地控制发生的事情?没有示例很难说,创建一个新问题并发布链接,我会尽力帮助解决它。 - Craig van Tonder
这个答案应该会有所帮助。你可以使用计算属性直接观察年龄。 - shaunak1111
7个回答

173

您的比较函数在旧值和新值之间存在一些问题。最好不要过于复杂化,因为这样会增加以后的调试工作量。您应该保持简单。

最好的方法是创建一个person-component,并在其自己的组件中单独监视每个人,如下所示:

<person-component :person="person" v-for="person in people"></person-component>

请见下面一个关于监视组件内部的人物的工作示例。如果您想在父元素上处理它,可以使用 $emit 将一个向上传递的事件,其中包含修改后的人物的 id

Vue.component('person-component', {
    props: ["person"],
    template: `
        <div class="person">
            {{person.name}}
            <input type='text' v-model='person.age'/>
        </div>`,
    watch: {
        person: {
            handler: function(newValue) {
                console.log("Person with ID:" + newValue.id + " modified")
                console.log("New age: " + newValue.age)
            },
            deep: true
        }
    }
});

new Vue({
    el: '#app',
    data: {
        people: [
          {id: 0, name: 'Bob', age: 27},
          {id: 1, name: 'Frank', age: 32},
          {id: 2, name: 'Joe', age: 38}
        ]
    }
});
<script src="https://unpkg.com/vue@2.1.5/dist/vue.js"></script>
<body>
    <div id="app">
        <p>List of people:</p>
        <person-component :person="person" v-for="person in people"></person-component>
    </div>
</body>


3
顺便说一句,感谢你花时间解释,这有助于我更多地了解Vue,我非常感激! - Craig van Tonder
1
我花了一些时间才理解这个,但你说得完全正确,这很有效,并且是避免混淆和进一步问题的正确方法 :) - Craig van Tonder
1
我也注意到了这一点,并有同样的想法,但对象中还包含值索引,其中包含值,getter和setter都在那里,但相比之下它忽略了它们,缺乏更好的理解,我认为它不评估任何原型。另一个答案提供了它不能工作的原因,因为newVal和oldVal是相同的东西,这有点复杂,但已经在几个地方得到了解决,另一个答案为易于创建用于比较目的的不可变对象提供了一个不错的解决方法。 - Craig van Tonder
1
最终,你的方法更容易一眼理解,并且在值改变时提供了更多的灵活性。这让我更好地理解了在Vue中保持简单的好处,但是像你在我的其他问题中看到的那样,我还有些困惑。非常感谢! :) - Craig van Tonder
@Mani,如果我们要监视一个人对象数组,你会如何让它工作?在你的例子中,你监视了person,但是如果你想监视像[{name: 'john'}, {name: 'Rick'}]这样的结构体上的属性,然后如果第二个人的名字改变为[{name: 'john'}, {name: 'Steve'}],你该如何监视呢? - CodeConnoisseur
显示剩余2条评论

24

这是一种明确定义的行为。您无法获取变异对象的旧值。这是因为newValoldVal都引用同一个对象。Vue不会保留您变异的对象的旧副本。

如果您用另一个对象替换了该对象,则Vue将为您提供正确的引用。

请阅读文档中的Note部分 (vm.$watch)

更多信息,请参见此处此处


3
哦,我的天啊,非常感谢!这个有点棘手……我完全预料到val和oldVal会不同,但在检查它们之后,我发现它们是新数组的两个副本,它没有在跟踪之前保留它。阅读了一些内容,找到了一个未被回答的S.O.问题,涉及相同的误解:http://stackoverflow.com/questions/35991494/find-out-which-property-changed-in-a-vue-js-deep-custom-directive - Craig van Tonder

24

我已经改变了它的实现方式,以解决你的问题,我创建了一个对象来跟踪旧的更改并将其与新的更改进行比较。您可以使用它来解决您的问题。

在这里,我创建了一个方法,在该方法中,旧值将被存储在一个单独的变量中,并且将在观察中使用它。

new Vue({
  methods: {
    setValue: function() {
      this.$data.oldPeople = _.cloneDeep(this.$data.people);
    },
  },
  mounted() {
    this.setValue();
  },
  el: '#app',
  data: {
    people: [
      {id: 0, name: 'Bob', age: 27},
      {id: 1, name: 'Frank', age: 32},
      {id: 2, name: 'Joe', age: 38}
    ],
    oldPeople: []
  },
  watch: {
    people: {
      handler: function (after, before) {
        // Return the object that changed
        var vm = this;
        let changed = after.filter( function( p, idx ) {
          return Object.keys(p).some( function( prop ) {
            return p[prop] !== vm.$data.oldPeople[idx][prop];
          })
        })
        // Log it
        vm.setValue();
        console.log(changed)
      },
      deep: true,
    }
  }
})

请查看已更新的CodePen


当它被挂载时,存储数据的副本并使用它来进行比较。有趣的是,我的用例会更加复杂,我不确定在向数组添加和删除对象时如何工作,@Quirk提供了解决问题的好链接。但我不知道你可以使用vm.$data,谢谢! - Craig van Tonder
是的,我在观察器更新后通过再次调用该方法进行更新,这样即使返回到原始值,它也会跟踪变化。 - Viplock
哦,我没有注意到那里隐藏着的东西,这样做非常有道理,而且是处理这个问题的一种不太复杂的方式(与github上的解决方案相比)。 - Craig van Tonder
而且,如果您正在添加或删除原始数组中的某些内容,只需再次调用该方法,您就可以再次使用解决方案。 - Viplock
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Craig van Tonder
1
_.cloneDeep() 在我的情况下真的很有帮助。谢谢!非常有用! - Cristiana Pereira

14

组件解决方案和深克隆解决方案各有优点,但也存在问题:

  1. 有时您希望跟踪抽象数据中的更改 - 围绕该数据构建组件并不总是有意义。

  2. 每次进行更改时,完全深度克隆数据结构可能非常昂贵。

我认为有更好的方法。如果您想监视列表中的所有项目并知道列表中更改了哪个项目,可以单独为每个项目设置自定义监视程序,如下所示:

var vm = new Vue({
  data: {
    list: [
      {name: 'obj1 to watch'},
      {name: 'obj2 to watch'},
    ],
  },
  methods: {
    handleChange (newVal) {
      // Handle changes here!
      console.log(newVal);
    },
  },
  created () {
    this.list.forEach((val) => {
      this.$watch(() => val, this.handleChange, {deep: true});
    });
  },
});

使用这种结构,handleChange() 将接收到已更改的具体列表项 - 从那里,您可以进行任何所需的处理。

我还在这里记录了一个更复杂的场景,以防您正在添加/删除项目到您的列表中(而不仅仅是操作已经存在的项目)。


1
谢谢Erik,你提出了有效的观点,所提供的方法肯定是有用的,如果作为问题的解决方案实施的话。 - Craig van Tonder
在这种情况下,它只会遵循created()的更改吗?而不是每次更改带有数组的对象的子属性时?我尝试过这个,但当我将其添加到created时,它没有记录任何内容到控制台。@ErikKoopmans - CodeConnoisseur
@PA-GW created 中的代码只是设置了观察器。在该代码运行之后,每当列表中的一个值发生更改(比如你设置了 this.list[0].name = 'new name'),handleChange 代码就会触发。 - Erik Koopmans
非常完美,这在获取/刷新数据后运行效果很好。 - Rob

10

这就是我用来深度观察对象的工具。

我的要求是观察对象的子字段。

new Vue({
    el: "#myElement",
    data:{
        entity: {
            properties: []
        }
    },
    watch:{
        'entity.properties': {
            handler: function (after, before) {
                // Changes detected.    
            },
            deep: true
        }
    }
});

2
我相信你可能没有理解 https://dev59.com/4lgR5IYBdhLWcg3wm-K6#41136186 中所描述的注意事项。只是为了明确,这不是问题的解决方案,并且在某些情况下不会像你期望的那样工作。 - Craig van Tonder
这正是我正在寻找的!谢谢。 - Jaydeep Shil
一样的,正是我所需要的!! 谢谢。 - Guntar
这正是我一直在寻找的!!!谢谢,伙计 ;) - Çağlar Duman

1
如果我们有对象或对象数组,并且希望在Vuejs中对它们进行监视,需要在watch中使用deep: true
    watch: {
      'Object.key': {
         handler (after, before) {
            // Changes detected. 
         },
         deep: true
       } 
     }

     watch: {
      array: {
         handler (after, before) {
             // Changes detected. 
         },
         deep: true
       } 
     }

0

我用"computed"解决了问题,而不是使用"watch"!

我还没有测试过这段代码,但我认为它应该可以工作。如果不能,请在评论中告诉我。

<script>
new Vue({
  el: '#app',
  data: {
    people: [
      {id: 0, name: 'Bob', age: 27},
      {id: 1, name: 'Frank', age: 32},
      {id: 2, name: 'Joe', age: 38}
    ],
    oldVal: {},
    peopleComputed: computed({
      get(){
        this.$data.oldVal = { ...people };
        return people;
      },
      set(val){
        // Return the object that changed
        var changed = val.filter( function( p, idx ) {
          return Object.keys(p).some( function( prop ) {
            return p[prop] !== this.$data.oldVal[idx][prop];
          })
        })
        // Log it
        console.log(changed)
        this.$data.people = val;
      }
    }),
  }
})
</script>

你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接