ChatGPT解决这个技术问题 Extra ChatGPT

Vue.js - How to properly watch for nested data

I'm trying to understand how to properly watch for some prop variation. I have a parent component (.vue files) that receive data from an ajax call, put the data inside an object and use it to render some child component through a v-for directive, below a simplification of my implementation:

<template>
    <div>
        <player v-for="(item, key, index) in players"
            :item="item"
            :index="index"
            :key="key"">
        </player>
    </div>
</template>

... then inside <script> tag:

 data(){
     return {
         players: {}
 },
 created(){
        let self = this;
        this.$http.get('../serv/config/player.php').then((response) => {
            let pls = response.body;
            for (let p in pls) {
                self.$set(self.players, p, pls[p]);
            }
    });
}

item objects are like this:

item:{
   prop: value,
   someOtherProp: {
       nestedProp: nestedValue,
       myArray: [{type: "a", num: 1},{type: "b" num: 6} ...]
    },
}

Now, inside my child "player" component I'm trying to watch for any Item's property variation and I use:

...
watch:{
    'item.someOtherProp'(newVal){
        //to work with changes in "myArray"
    },
    'item.prop'(newVal){
        //to work with changes in prop
    }
}

It works but it seems a bit tricky to me and I was wondering if this is the right way to do it. My goal is to perform some action every time prop changes or myArray gets new elements or some variation inside existing ones. Any suggestion will be appreciated.


t
tony19

You can use a deep watcher for that:

watch: {
  item: {
     handler(val){
       // do stuff
     },
     deep: true
  }
}

This will now detect any changes to the objects in the item array and additions to the array itself (when used with Vue.set). Here's a JSFiddle: http://jsfiddle.net/je2rw3rs/

EDIT

If you don't want to watch for every change on the top level object, and just want a less awkward syntax for watching nested objects directly, you can simply watch a computed instead:

var vm = new Vue({
  el: '#app',
  computed: {
    foo() {
      return this.item.foo;
    }
  },
  watch: {
    foo() {
      console.log('Foo Changed!');
    }
  },
  data: {
    item: {
      foo: 'foo'
    }
  }
})

Here's the JSFiddle: http://jsfiddle.net/oa07r5fw/


For the deep watcher, the watcher takes args newValue, oldValue, but for objects I've found that it passes the same value both args. (Probably for performance--they don't want to do a deep copy of the structure.) This means you can't tell why then handler was called based on the args. (If that is a concern for you.)
M
Marcello B.

Another good approach and one that is a bit more elegant is as follows:

 watch:{
     'item.someOtherProp': function (newVal, oldVal){
         //to work with changes in someOtherProp
     },
     'item.prop': function(newVal, oldVal){
         //to work with changes in prop
     }
 }

(I learned this approach from @peerbolte in the comment here)


The official documentation includes this approach now
watch fires just the first time, why? @feihcsim
the property i was watching wasnt reactive, i turned to and it work!
+1 for this solution, and just remembering you can use this syntax as well: 'item.someOtherProp'(newVal, oldVal){ .. }
A
Alper Ebicoglu

VueJs deep watch in child objects

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

L
LastM4N

Personally I prefer this clean implementation:

watch: {
  myVariable: {
     handler(newVal, oldVal){  // here having access to the new and old value
       // do stuff
     },
     deep: true,
     immediate: true //  Also very important the immediate in case you need it, the callback will be called immediately after the start of the observation

  }
}

Thanks. This works perfectly. Although I found that you can listen to a nested value by listening to the string as the myVariable name. 'myObject.property': { handler(new, old) { // do stuff here } }
t
tony19

How if you want to watch a property for a while and then to un-watch it? Or to watch a library child component property?

You can use the "dynamic watcher":

this.$watch(
 'object.property', //what you want to watch
 (newVal, oldVal) => {
    //execute your code here
 }
)

The $watch returns an unwatch function which will stop watching if it is called.

var unwatch = vm.$watch('a', cb)
// later, teardown the watcher
unwatch()

Also you can use the deep option:

this.$watch(
'someObject', () => {
    //execute your code here
},
{ deep: true }
)

Please make sure to take a look to docs


According to the docs, you should not use arrow functions to define a watcher: Note that you should not use an arrow function to define a watcher (e.g. searchQuery: newValue => this.updateAutocomplete(newValue)). The reason is arrow functions bind the parent context, so this will not be the Vue instance as you expect and this.updateAutocomplete will be undefined.
Z
Zac

Another way to add that I used to 'hack' this solution was to do this: I set up a seperate computed value that would simply return the nested object value.

data : function(){
    return {
        countries : {
            UnitedStates : {
                value: "hello world";
            }.
        },
    };
},
computed : {
    helperName : function(){
        return this.countries.UnitedStates.value;
    },
},
watch : {
    helperName : function(newVal, oldVal){
        // do this...
    }
}

I just needed to watch only one nested data from object, so this is just right answer to me. Thank you
E
Erik Koopmans

Tracking individual changed items in a list

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, oldVal) {
      // Handle changes here!
      // NOTE: For mutated objects, newVal and oldVal will be identical.
      console.log(newVal);
    },
  },
  created () {
    this.list.forEach((val) => {
      this.$watch(() => val, this.handleChange, {deep: true});
    });
  },
});

If your list isn't populated straight away (like in the original question), you can move the logic out of created to wherever needed, e.g. inside the .then() block.

Watching a changing list

If your list itself updates to have new or removed items, I've developed a useful pattern that "shallow" watches the list itself, and dynamically watches/unwatches items as the list changes:

// NOTE: This example uses Lodash (_.differenceBy and _.pull) to compare lists
//       and remove list items. The same result could be achieved with lots of
//       list.indexOf(...) if you need to avoid external libraries.

var vm = new Vue({
  data: {
    list: [
      {name: 'obj1 to watch'},
      {name: 'obj2 to watch'},
    ],
    watchTracker: [],
  },
  methods: {
    handleChange (newVal, oldVal) {
      // Handle changes here!
      console.log(newVal);
    },
    updateWatchers () {
      // Helper function for comparing list items to the "watchTracker".
      const getItem = (val) => val.item || val;

      // Items that aren't already watched: watch and add to watched list.
      _.differenceBy(this.list, this.watchTracker, getItem).forEach((item) => {
        const unwatch = this.$watch(() => item, this.handleChange, {deep: true});
        this.watchTracker.push({ item: item, unwatch: unwatch });
        // Uncomment below if adding a new item to the list should count as a "change".
        // this.handleChange(item);
      });

      // Items that no longer exist: unwatch and remove from the watched list.
      _.differenceBy(this.watchTracker, this.list, getItem).forEach((watchObj) => {
        watchObj.unwatch();
        _.pull(this.watchTracker, watchObj);
        // Optionally add any further cleanup in here for when items are removed.
      });
    },
  },
  watch: {
    list () {
      return this.updateWatchers();
    },
  },
  created () {
    return this.updateWatchers();
  },
});

g
getglad

Not seeing it mentioned here, but also possible to use the vue-property-decorator pattern if you are extending your Vue class.

import { Watch, Vue } from 'vue-property-decorator';

export default class SomeClass extends Vue {
   ...

   @Watch('item.someOtherProp')
   someOtherPropChange(newVal, oldVal) {
      // do something
   }

   ...
}

J
Jonathan Arias

I've found it works this way too:

watch: {
    "details.position"(newValue, oldValue) {
        console.log("changes here")
    }
},
data() {
    return {
      details: {
          position: ""
      }
    }
}

k
krubo

My problem with the accepted answer of using deep: true, is that when deep-watching an array, I can't easily identify which element of the array contains the change. The only clear solution I've found is this answer, which explains how to make a component so you can watch each array element individually.


Yes that is true, but that's not what a deep watcher is for (in fact you'll find oldVal and newVal both reference the same object, so are the same). The intention of a deep watcher is to do something when the values change, such as emit an event, make an ajax call etc. Otherwise, you probably want a component, as pointed out in Mani's answer.
I had the same problem of tracking the individual changes! I found a solution though, and documented it here.
t
thomasL

None of the answer for me was working. Actually if you want to watch on nested data with Components being called multiple times. So they are called with different props to identify them. For example <MyComponent chart="chart1"/> <MyComponent chart="chart2"/> My workaround is to create an addionnal vuex state variable, that I manually update to point to the property that was last updated.

Here is a Vuex.ts implementation example:

export default new Vuex.Store({
    state: {
        hovEpacTduList: {},  // a json of arrays to be shared by different components, 
                             // for example  hovEpacTduList["chart1"]=[2,6,9]
        hovEpacTduListChangeForChart: "chart1"  // to watch for latest update, 
                                                // here to access "chart1" update 
   },
   mutations: {
        setHovEpacTduList: (state, payload) => {
            state.hovEpacTduListChangeForChart = payload.chart // we will watch hovEpacTduListChangeForChart
            state.hovEpacTduList[payload.chart] = payload.list // instead of hovEpacTduList, which vuex cannot watch
        },
}

On any Component function to update the store:

    const payload = {chart:"chart1", list: [4,6,3]}
    this.$store.commit('setHovEpacTduList', payload);

Now on any Component to get the update:

    computed: {
        hovEpacTduListChangeForChart() {
            return this.$store.state.hovEpacTduListChangeForChart;
        }
    },
    watch: {
        hovEpacTduListChangeForChart(chart) {
            if (chart === this.chart)  // the component was created with chart as a prop <MyComponent chart="chart1"/> 
                console.log("Update! for", chart, this.$store.state.hovEpacTduList[chart]);
        },
    },

R
Rahil Wazir

For anyone looking for Vue 3


import { watch } from 'vue';

...
...

watch(
  () => yourNestedObject,              // first param, your object
  (currValue, prevValue) => {          // second param, watcher callback
    console.log(currValue, prevValue);
  },
  { deep: true }                       // third param, for deep checking
);

You can refer to the documentation here: https://v3.vuejs.org/guide/reactivity-computed-watchers.html#watch


J
Jonathan Ruiz

I used deep:true, but found the old and new value in the watched function was the same always. As an alternative to previous solutions I tried this, which will check any change in the whole object by transforming it to a string:

created() {
    this.$watch(
        () => JSON.stringify(this.object),
            (newValue, oldValue) => {
                //do your stuff                
            }
    );
},

T
Tess E. Duursma

Here's a way to write watchers for nested properties:

    new Vue({
        ...allYourOtherStuff,
        watch: {
            ['foo.bar'](newValue, oldValue) {
                // Do stuff here
            }
        }
    });

You can even use this syntax for asynchronous watchers:

    new Vue({
        ...allYourOtherStuff,
        watch: {
            async ['foo.bar'](newValue, oldValue) {
                // Do stuff here
            }
        }
    });