Vue - Regarder en profondeur un éventail d'objets et calculer le changement?


108

J'ai un tableau appelé peoplequi contient des objets comme suit:

Avant

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

Cela peut changer:

Après

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

Remarquez que Frank vient d'avoir 33 ans.

J'ai une application dans laquelle j'essaie de regarder le tableau des personnes et lorsque l'une des valeurs change, enregistrez le changement:

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

Je me suis basé sur la question que j'ai posée hier sur les comparaisons de tableaux et j'ai sélectionné la réponse la plus rapide.

Donc, à ce stade, je m'attends à voir un résultat de: { id: 1, name: 'Frank', age: 33 }

Mais tout ce que je reçois dans la console est (en gardant à l'esprit que je l'avais dans un composant):

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

Et dans le codepen que j'ai créé , le résultat est un tableau vide et non l'objet modifié qui a changé, ce à quoi je m'attendais.

Si quelqu'un pouvait suggérer pourquoi cela se produit ou où je me suis trompé ici, ce serait grandement apprécié, merci beaucoup!

Réponses:


136

Votre fonction de comparaison entre l'ancienne valeur et la nouvelle valeur a un problème. Il vaut mieux ne pas trop compliquer les choses, car cela augmentera votre effort de débogage plus tard. Vous devez rester simple.

Le meilleur moyen est de créer person-componentet de regarder chaque personne séparément dans son propre composant, comme indiqué ci-dessous:

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

Veuillez trouver ci-dessous un exemple de travail pour regarder un composant de personne à l'intérieur. Si vous souhaitez le gérer du côté parent, vous pouvez utiliser $emitpour envoyer un événement vers le haut, contenant la idpersonne modifiée.

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>


C'est en effet une solution de travail mais ce n'est pas entièrement conforme à mon cas d'utilisation. Vous voyez, en réalité, j'ai l'application et un composant, le composant utilise la table vue-material et répertorie les données avec la possibilité de modifier les valeurs en ligne. J'essaie de changer l'une des valeurs puis de vérifier ce qui a changé, donc dans ce cas, il compare en fait les tableaux avant et après pour voir quelle différence il y a. Puis-je implémenter votre solution pour résoudre le problème? En effet, je pourrais probablement le faire, mais je pense simplement que cela fonctionnerait à contre-courant de ce qui est disponible à cet égard dans vue-material
Craig van Tonder

2
Au fait, merci d'avoir pris le temps de l'expliquer, cela m'a aidé à en savoir plus sur Vue que j'apprécie!
Craig van Tonder

Il m'a fallu un certain temps pour comprendre cela mais vous avez absolument raison, cela fonctionne comme un charme et c'est la bonne façon de faire les choses si vous voulez éviter la confusion et d'autres problèmes :)
Craig van Tonder

1
J'ai remarqué cela aussi et j'ai eu la même pensée mais ce qui est également contenu dans l'objet est l'indice de valeur qui contient la valeur, les getters et les setters sont là mais en comparaison, il les ignore, faute de meilleure compréhension, je pense que c'est le cas ne pas évaluer sur des prototypes. L'une des autres réponses fournit la raison pour laquelle cela ne fonctionnerait pas, c'est parce que newVal et oldVal étaient la même chose, c'est un peu compliqué mais c'est quelque chose qui a été abordé à quelques endroits, encore une autre réponse fournit un travail décent pour une création facile. un objet immuable à des fins de comparaison.
Craig van Tonder

1
En fin de compte, votre chemin est plus facile à comprendre en un coup d'œil et offre plus de flexibilité en termes de ce qui est disponible lorsque la valeur change. Cela m'a beaucoup aidé à comprendre les avantages de rester simple dans Vue, mais je suis resté un peu coincé, comme vous l'avez vu dans mon autre question. Merci beaucoup! :)
Craig van Tonder

21

J'ai changé l'implémentation de celui-ci pour résoudre votre problème, j'ai créé un objet pour suivre les anciennes modifications et le comparer avec cela. Vous pouvez l'utiliser pour résoudre votre problème.

Ici, j'ai créé une méthode dans laquelle l'ancienne valeur sera stockée dans une variable séparée et, qui sera ensuite utilisée dans une montre.

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

Voir le codepen mis à jour


Ainsi, quand il est monté, stockez une copie des données et utilisez-les pour les comparer. Intéressant, mais mon cas d'utilisation serait plus complexe et je ne suis pas sûr de savoir comment cela fonctionnerait lors de l'ajout et de la suppression d'objets du tableau, @Quirk a également fourni de bons liens pour résoudre le problème. Mais je ne savais pas que vous pouvez utiliser vm.$data, merci!
Craig van Tonder

oui, et je le mets à jour après la montre également en appelant à nouveau la méthode, si vous revenez à la valeur d'origine, il suivra également le changement.
Viplock

Ohhh, je n'ai pas remarqué que se cacher là-bas, a beaucoup de sens et est une façon moins compliquée de gérer cela (par opposition à la solution sur github).
Craig van Tonder

et oui, si vous ajoutez ou supprimez quelque chose du tableau d'origine, appelez à nouveau la méthode et vous êtes prêt à reprendre la solution.
Viplock

1
_.cloneDeep () a vraiment aidé dans mon cas. Je vous remercie!! Très utile!
Cristiana Pereira

18

C'est un comportement bien défini. Vous ne pouvez pas obtenir l'ancienne valeur d'un objet muté . En effet , à la fois la newValet se oldValréfèrent au même objet. Vue ne conservera pas une ancienne copie d'un objet que vous avez muté.

Si vous aviez remplacé l'objet par un autre, Vue vous aurait fourni les références correctes.

Lisez la Notesection dans la documentation. ( vm.$watch)

Plus d'informations ici et ici .


3
Oh mon chapeau, merci beaucoup! C'est une question délicate ... Je m'attendais complètement à ce que val et oldVal soient différents, mais après les avoir inspectés, je vois que ce sont deux copies du nouveau tableau, il n'en a pas suivi avant. Lisez un peu plus et trouvez cette question SO sans réponse concernant le même malentendu: stackoverflow.com/questions/35991494/…
Craig van Tonder

5

C'est ce que j'utilise pour observer en profondeur un objet. Mon exigence était de regarder les champs enfants de l'objet.

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

Je pense que vous manquez peut-être la compréhension du cavet qui a été décrit dans stackoverflow.com/a/41136186/2110294 . Juste pour être clair, ce n'est pas une solution à la question et ne fonctionnera pas comme vous vous en doutez dans certaines situations.
Craig van Tonder le

c'est exactement ce que je cherchais !. Merci
Jaydeep Shil

La même chose ici, exactement ce dont j'avais besoin !! Merci.
Guntar le

4

La solution de composant et la solution de clone profond ont leurs avantages, mais aussi des problèmes:

  1. Parfois, vous souhaitez suivre les modifications des données abstraites - il n'est pas toujours logique de créer des composants autour de ces données.

  2. Le clonage en profondeur de l'ensemble de votre structure de données à chaque fois que vous apportez une modification peut être très coûteux.

Je pense qu'il y a une meilleure façon. 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 ceci:

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

Avec cette structure, handleChange()recevra l'élément de liste spécifique qui a changé - à partir de là, vous pouvez faire n'importe quelle manipulation que vous voulez.

J'ai également documenté un scénario plus complexe ici , au cas où vous ajouteriez / supprimeriez des éléments à votre liste (plutôt que de manipuler uniquement les éléments déjà présents).


Merci Erik, vous présentez des points valables et la méthodologie fournie est très certainement utile si elle est mise en œuvre comme solution à la question.
Craig van Tonder le
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.