ChatGPT解决这个技术问题 Extra ChatGPT

Vue - Deep watching an array of objects and calculating the change?

I have an array called people that contains objects as follows:

Before

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

It can change:

After

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

Notice Frank just turned 33.

I have an app where I am trying to watch the people array and when any of the values changes then log the change:

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

I based this on the question that I asked yesterday about array comparisons and selected the quickest working answer.

So, at this point I expect to see a result of: { id: 1, name: 'Frank', age: 33 }

But all I get back in the console is (bearing in mind i had it in a component):

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

And in the codepen that I made, the result is an empty array and not the changed object that changed which would be what I expected.

If anyone could suggest why this is happening or where I have gone wrong here then it would be greatly appreciated, many thanks!

my only problem with all the solutions below is that they fire on each change, in my case i want to make a database API call to update changed items only. an input v-model will fire 100s of times, i want to record the changes as they happen but save them to database when user clicks apply changes, how would you go about that
@PirateApp Not sure I follow your question exactly but perhaps this solves the issue that you're having: vuejs.org/v2/guide/forms.html#lazy. You could also opt to take deeper control over what happens by instead using the select / change events to trigger your logic? It's hard to say without an example, make a new question with come code and post a link, i'll try to help to solve it.
This answer should help. You can watch age directly using computed property stackoverflow.com/questions/52282586/…

M
Mani

Your comparison function between old value and new value is having some issue. It is better not to complicate things so much, as it will increase your debugging effort later. You should keep it simple.

The best way is to create a person-component and watch every person separately inside its own component, as shown below:

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

Please find below a working example for watching inside person component. If you want to handle it on parent side, you may use $emit to send an event upwards, containing the id of modified person.

Vue.component('person-component', { props: ["person"], template: `

{{person.name}}
`, 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} ] } });

List of people:


That is indeed a working solution but it's not entirely according to my use case. You see, in actuality I have the app and one component, the component uses vue-material table and lists the data with the ability to edit the values in-line. I am trying to change one of the values then check what changed so in this case, it does actually compare the before and after arrays to see what difference there is. Could I implement your solution to solve the problem? Indeed I could likely do so but just feel that it would be working against the flow of what is available in this regard within vue-material
By the way, thanks for taking the time to explain this, it's helped me to learn more about Vue which I do appreciate!
It took me a while to comprehend this but you're absolutely right, this works like a charm and is the correct way to do things if you want to avoid confusion and further issues :)
I did notice this too and had the same thought but what is also contained in the object is the value index which contains the value, the getters and setters are there but in comparison it disregards them, for lack of a better understanding I think it does not evaluate on any prototypes. One of the other answers provides the reason why it would not work, it's because newVal and oldVal were the same thing, it's a bit complicated but is something thats been addressed in a few places, yet another answer provides a decent work around for easy creating an immutable object for comparison purposes.
Ultimately though, your way is easier to comprehend at a glance and provides more flexibility in terms of what is available when the value changes. It's helped me a lot to understand the benefits of keeping it simple in Vue but got stuck with it a little as you saw in my other question. Many thanks! :)
F
Ferrybig

I have changed the implementation of it to get your problem solved, I made an object to track the old changes and compare it with that. You can use it to solve your issue.

Here I created a method, in which the old value will be stored in a separate variable and, which then will be used in a watch.

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,
    }
  }
})

See the updated codepen


So when it's mounted store a copy of the data and use this to compare against it. Interesting, but my use case would be more complex and i'm unsure of how that would work when adding and removing objects from the array, @Quirk provided good links to solve the problem too. But I didn't know that you can use vm.$data, thank you!
yes, and i am updating it after the watch also by calling the method again , by it if you go back to the original value , then also it will track the change.
Ohhh, I did not notice that hiding there, makes much sense and is a less complicated way to deal with this (as opposed to the solution on github).
and ya , if you are adding or removing something from the original array , just call the method again and you are good to go with the solution again.
_.cloneDeep() really helped in my case. Thank you!! Really helpful!
t
tony19

It is well defined behaviour. You cannot get the old value for a mutated object. That's because both the newVal and oldVal refer to the same object. Vue will not keep an old copy of an object that you mutated.

Had you replaced the object with another one, Vue would have provided you with correct references.

Read the Note section in the docs. (vm.$watch)

More on this here and here.


Oh my hat, Thanks so much! That is a tricky one... I completely expected val and oldVal to be different but after inspecting them I see it's two copies of the new array, it doesn't keep track of it before. Read a bit more and found this unanswered S.O. question regarding the same misunderstanding: stackoverflow.com/questions/35991494/…
E
Erik Koopmans

The component solution and deep-clone solution have their advantages, but also have issues:

Sometimes you want to track changes in abstract data - it doesn't always make sense to build components around that data. Deep-cloning your entire data structure every time you make a change can be very expensive.

I think there's a better way. If you want to watch all items in a list and know which item in the list changed, you can set up custom watchers on every item separately, like so:

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});
    });
  },
});

With this structure, handleChange() will receive the specific list item that changed - from there you can do any handling you like.

I have also documented a more complex scenario here, in case you are adding/removing items to your list (rather than only manipulating the items already there).


Thanks Erik, you present valid points and the methodology provided is most definitely useful if implemented as a solution to the question.
In this case would it only follow changes on created() thought? and not every time a sub property on an object with an array changes? I tried this out and it did not log anything to the console when I added it in created @ErikKoopmans
@PA-GW the code in created just sets up the watchers. After that code runs, any time one of the values in the list changes (like you set this.list[0].name = 'new name'), the handleChange code will trigger.
A
Alper Ebicoglu

This is what I use to deep watch an object. My requirement was watching the child fields of the object.

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

I believe that you might be missing the understanding of the cavet which was described in stackoverflow.com/a/41136186/2110294. Just to be clear, this is not a solution to the question and will not work as you may have expected in certain situations.
this is what exactly I was looking!. Thanks
The same here, exactly what I needed!! Thanks.
This is what exactly I was looking!!! Thanks man ;)
M
Mohammadreza Khalili

if we have Object or Array of object and we want to watch them in Vuejs or NUXTJS need to use deep: true in watch

    watch: {
      'Object.key': {
         handler (val) {
             console.log(val)
         },
         deep: true
       } 
     }

     watch: {
      array: {
         handler (val) {
             console.log(val)
         },
         deep: true
       } 
     }

m
menzel-m4m

Instead of "watch" I solved the problem with "computed"!

I have not tested this code but i think it should work. Please tell me in the comments if not.

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

Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.