Vue.js - Comment surveiller correctement les données imbriquées

414

J'essaie de comprendre comment surveiller correctement certaines variations d'accessoires. J'ai un composant parent (fichiers .vue) qui reçoit des données d'un appel ajax, place les données dans un objet et les utilise pour rendre un composant enfant via une directive v-for, sous une simplification de mon implémentation:

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

... puis à l'intérieur de la <script>balise:

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

les objets objets sont comme ceci:

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

Maintenant, à l'intérieur du composant "joueur" de mon enfant, j'essaye de surveiller la variation de propriété d'un objet et j'utilise:

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

Cela fonctionne mais cela me semble un peu délicat et je me demandais si c'était la bonne façon de procéder. Mon objectif est d'effectuer une action à chaque fois que des propchangements ou myArraydes éléments nouveaux ou des variations à l'intérieur de ceux existants sont modifiés. N'hésitez pas à nous faire part de vos suggestions.

Plastique
la source
9
Utilisez simplement "item.someOtherProp": function (newVal, oldVal){comme suggéré par @Ron.
Reiner
1
@Reiner, c'est exactement ce qu'il essayait d'éviter dans la question; c'est la même chose en utilisant la syntaxe ES5. En fait, il n'y a pas de frais supplémentaires pour regarder un calcul et c'est une technique utile dans diverses situations.
craig_h
1
Est-il possible de surveiller un changement dans tout l' keysintérieur d'un objet? Exemple:"item.*": function(val){...}
Charlie

Réponses:

623

Vous pouvez utiliser un observateur profond pour cela:

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

Cela détectera désormais toutes les modifications apportées aux objets du itemtableau et les ajouts au tableau lui-même (lorsqu'il est utilisé avec Vue.set ). Voici un JSFiddle: http://jsfiddle.net/je2rw3rs/

ÉDITER

Si vous ne souhaitez pas surveiller chaque modification de l'objet de niveau supérieur et souhaitez simplement une syntaxe moins gênante pour regarder directement les objets imbriqués, vous pouvez simplement regarder un à la computedplace:

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

Voici le JSFiddle: http://jsfiddle.net/oa07r5fw/

craig_h
la source
2
de cette façon, "handler" sera invoqué chaque fois que n'importe quel accessoire a une variation, mon but est de séparer les gestionnaires afin d'observer si les changements arrivent à "prop" ou dans myArray dans "someOtherProp" séparément
Plastique
9
Si vous voulez juste surveiller les changements sur des objets imbriqués spécifiques, alors ce que vous faites est très bien. Si vous voulez une syntaxe moins gênante, vous pouvez regarder un à la computedplace: jsfiddle.net/c52nda7x
craig_h
Exactement ce que je cherchais, maintenant cela me semble tellement logique: créer une propriété calculée qui renvoie l'accessoire imbriqué et le surveiller. Merci beaucoup.
Plastique
Je veux juste ajouter - que la deuxième option n'est vraiment pas très différente de l'observateur d'origine. En interne, une propriété calculée n'est qu'un observateur de tous les objets / valeurs référencés dans la fonction qui définit une valeur de données pour le résultat de la fonction. - Donc, vous ne faites probablement que rendre le code un peu plus compliqué avec le même effet.
Falco
22
@Falco Je viens de découvrir la réponse dans un commentaire ici . peerbolte a indiqué que cela peut être fait en mettant le nom de la montre entre guillemets simples. J'ai testé cela et cela fonctionne. Donc, dans ce cas, ce serait watch: { 'item.foo' : function(newVal, oldVal) { // do work here }} assez lisse. J'adore Vue!
Ron C
437

Une autre bonne approche et qui est un peu plus élégante est la suivante:

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

(J'ai appris cette approche de @peerbolte dans le commentaire ici )

Ron C
la source
47
beaucoup plus élégant (pas seulement un peu): P. Cela devrait être dans la documentation Vue.js car c'est un cas d'utilisation courant. Je cherche depuis des heures une solution, merci pour votre réponse.
Claudiu
11
@symba Il est intéressant que les gens n'aient pas remarqué que c'est la même chose que l'OP a demandé d'éviter, juste dans la ES5syntaxe. Nous pourrions bien sûr affirmer que le ES2015 method definitionlui-même n'est pas très élégant, mais il manque en quelque sorte le point de la question, qui est de savoir comment éviter de mettre le nom entre guillemets. Je suppose que la plupart des gens passent sous silence la question d'origine, qui contient déjà la réponse, et recherchent en fait quelque chose qui, comme vous le dites, n'est pas très bien documenté,
craig_h
1
@craig_h point super intéressant. Je cherchais à l'origine pendant des heures pour trouver un bon moyen de regarder un accessoire imbriqué. Et ce titre de question était la question la plus proche que j'ai trouvée, mais j'ai manqué qu'il ait énuméré un accessoire cité dans le corps de la question mais n'a pas trouvé les réponses si élégantes. J'ai finalement trouvé l'approche d'hélice citée dans le commentaire d'une autre question et j'ai été tenté de créer une question juste pour y répondre moi-même en tant que documentation de l'approche d'hélice citée sur SO. Mais cette question était exactement quel aurait été mon titre, alors j'ai ajouté la réponse ici. J'aurais peut-être dû créer une nouvelle question.
Ron C
9
@RonC C'est une question populaire et votre réponse est exactement ce que beaucoup de gens recherchent, donc je pense que ça se passe bien ici. Nous avons tous seulement un temps limité, et la plupart d'entre nous lisent à peine les questions, donc ce n'était certainement pas une critique de la réponse que vous avez fournie, je suis juste un peu pédant en soulignant que ma réponse originale était spécifique à la question et n'était pas destiné à être exprimé. En fait, j'aime l'approche «regarder calculé», cependant, beaucoup préfèrent l'approche «citations» moins compliquée, que vous avez résumée succinctement dans votre réponse.
craig_h
10
La documentation officielle inclut cette approche maintenant
feihcsim
20

VueJs deep watch dans les objets enfants

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

Comment faire si vous voulez regarder une propriété pendant un certain temps, puis la retirer?

Ou pour regarder une propriété de composant enfant de bibliothèque?

Vous pouvez utiliser "l'observateur dynamique":

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

La $watchrenvoie une fonction de non-surveillance qui arrêtera de regarder si elle est appelée.

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

Vous pouvez également utiliser l' deepoption:

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

Veuillez vous assurer de consulter les documents

roli roli
la source
2
Selon la documentation , vous ne devez pas utiliser les fonctions fléchées pour définir un observateur: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.
wonder95
7

Ne le voyant pas mentionné ici, mais également possible d'utiliser le vue-property-decoratormodèle si vous étendez votre Vueclasse.

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

export default class SomeClass extends Vue {
   ...

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

   ...
}
getglad
la source
5

Une autre façon d'ajouter que j'avais l'habitude de «pirater» cette solution était de le faire: j'ai configuré une computedvaleur distincte qui retournerait simplement la valeur de l'objet imbriqué.

data : function(){
    return {
        my_object : {
            my_deep_object : {
                my_value : "hello world";
            }.
        },
    };
},
computed : {
    helper_name : function(){
        return this.my_object.my_deep_object.my_value;
    },
},
watch : {
    helper_name : function(newVal, oldVal){
        // do this...
    }
}
Zac
la source
bon;) mais qu'en est-il des tableaux? :)
WEBCENTER
3

Mon problème avec la réponse acceptée d'utilisation deep: true, c'est que lorsque je regarde un tableau en profondeur, je ne peux pas facilement identifier quel élément du tableau contient le changement. La seule solution claire que j'ai trouvée est cette réponse, qui explique comment créer un composant afin que vous puissiez regarder chaque élément du tableau individuellement.

krubo
la source
1
Oui, c'est vrai, mais ce n'est pas à cela que sert un observateur profond (en fait, vous trouverez oldValet newValréférencerez tous deux le même objet, ils sont donc les mêmes). L'intention de a deep watcherest de faire quelque chose lorsque les valeurs changent, comme émettre un événement, effectuer un appel ajax, etc. Sinon, vous voulez probablement un composant, comme indiqué dans la réponse de Mani.
craig_h
J'ai eu le même problème de suivi des modifications individuelles! J'ai cependant trouvé une solution et je l'ai documentée ici .
Erik Koopmans
3

Suivi des éléments modifiés individuellement dans une liste

Si vous souhaitez regarder tous les éléments d'une liste et savoir quel élément de la liste a changé, vous pouvez configurer des observateurs personnalisés sur chaque élément séparément, comme suit:

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

Si votre liste n'est pas remplie immédiatement (comme dans la question d'origine), vous pouvez déplacer la logique hors de l' createdendroit où vous en avez besoin, par exemple à l'intérieur du.then() bloc.

Regarder une liste changeante

Si votre liste elle-même se met à jour pour avoir des éléments nouveaux ou supprimés, j'ai développé un modèle utile qui "superficiel" surveille la liste elle-même, et surveille / décompresse dynamiquement les éléments lorsque la liste change:

// 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();
  },
});
Erik Koopmans
la source
0

Aucune des réponses pour moi ne fonctionnait. En fait, si vous souhaitez surveiller les données imbriquées avec des composants appelés plusieurs fois. Ils sont donc appelés avec différents accessoires pour les identifier. Par exemple<MyComponent chart="chart1"/> <MyComponent chart="chart2"/> ma solution consiste à créer une variable d'état vuex supplémentaire, que je mets à jour manuellement pour pointer vers la dernière propriété mise à jour.

Voici un exemple d'implémentation de Vuex.ts:

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

Sur n'importe quelle fonction de composant pour mettre à jour le magasin:

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

Maintenant, sur n'importe quel composant pour obtenir la mise à jour:

    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]);
        },
    },
thomasL
la source