Vue - ¿Observa profundamente una serie de objetos y calcula el cambio?

108

Tengo una matriz llamada peopleque contiene objetos de la siguiente manera:

antes de

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

Puede cambiar:

Después

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

Note que Frank acaba de cumplir 33 años.

Tengo una aplicación en la que estoy tratando de ver la matriz de personas y cuando alguno de los valores cambia, entonces registro el cambio:

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

Basé esto en la pregunta que hice ayer sobre las comparaciones de matrices y seleccioné la respuesta de trabajo más rápida.

Entonces, en este punto espero ver un resultado de: { id: 1, name: 'Frank', age: 33 }

Pero todo lo que obtengo en la consola es (teniendo en cuenta que lo tenía en un componente):

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

Y en el codepen que hice , el resultado es una matriz vacía y no el objeto modificado que cambió, que sería lo que esperaba.

Si alguien pudiera sugerir por qué está sucediendo esto o dónde me he equivocado aquí, sería muy apreciado, ¡muchas gracias!

Craig van Tonder
fuente

Respuestas:

136

Su función de comparación entre el valor antiguo y el nuevo valor está teniendo algún problema. Es mejor no complicar tanto las cosas, ya que aumentará su esfuerzo de depuración más adelante. Deberías mantenerlo simple.

La mejor manera es crear person-componenty observar a cada persona por separado dentro de su propio componente, como se muestra a continuación:

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

A continuación, encontrará un ejemplo práctico para observar el componente de persona interna. Si desea manejarlo en el lado de los padres, puede usar $emitpara enviar un evento hacia arriba, que contenga la idde la persona modificada.

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/[email protected]/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>

Mani
fuente
De hecho, esa es una solución que funciona, pero no está completamente de acuerdo con mi caso de uso. Verá, en realidad tengo la aplicación y un componente, el componente usa la tabla vue-material y enumera los datos con la capacidad de editar los valores en línea. Estoy tratando de cambiar uno de los valores y luego verificar qué cambió, por lo que en este caso, realmente compara las matrices antes y después para ver qué diferencia hay. ¿Puedo implementar su solución para resolver el problema? De hecho, probablemente podría hacerlo, pero siento que estaría trabajando en contra del flujo de lo que está disponible a este respecto en vue-material
Craig van Tonder
2
Por cierto, gracias por tomarse el tiempo para explicar esto, ¡me ha ayudado a aprender más sobre Vue, lo cual aprecio!
Craig van Tonder
Me tomó un tiempo comprender esto, pero tienes toda la razón, esto funciona como un encanto y es la forma correcta de hacer las cosas si quieres evitar confusiones y más problemas :)
Craig van Tonder
1
Me di cuenta de esto también y tuve el mismo pensamiento, pero lo que también está contenido en el objeto es el índice de valor que contiene el valor, los captadores y definidores están ahí, pero en comparación los ignora, por falta de una mejor comprensión, creo que sí. no evaluar en ningún prototipo. Una de las otras respuestas proporciona la razón por la que no funcionaría, es porque newVal y oldVal eran lo mismo, es un poco complicado pero es algo que se ha abordado en algunos lugares, otra respuesta proporciona un trabajo decente para crear fácilmente un objeto inmutable para fines de comparación.
Craig van Tonder
1
Sin embargo, en última instancia, su camino es más fácil de comprender de un vistazo y proporciona más flexibilidad en términos de lo que está disponible cuando cambia el valor. Me ha ayudado mucho a comprender los beneficios de mantenerlo simple en Vue, pero me quedé atascado un poco como vio en mi otra pregunta. ¡Muchas gracias! :)
Craig van Tonder
21

He cambiado la implementación para resolver su problema, hice un objeto para rastrear los cambios anteriores y compararlo con eso. Puede usarlo para resolver su problema.

Aquí creé un método, en el que el valor anterior se almacenará en una variable separada y, luego, se usará en un reloj.

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

Ver el codepen actualizado

Viplock
fuente
Entonces, cuando esté montado, almacene una copia de los datos y utilícelo para compararlos. Interesante, pero mi caso de uso sería más complejo y no estoy seguro de cómo funcionaría al agregar y eliminar objetos de la matriz, @Quirk también proporcionó buenos enlaces para resolver el problema. Pero no sabía que podías usar vm.$data, ¡gracias!
Craig van Tonder
sí, y lo estoy actualizando después del reloj también llamando al método nuevamente, si regresa al valor original, también rastreará el cambio.
Viplock
Ohhh, no me di cuenta de que esconderse allí tiene mucho sentido y es una forma menos complicada de lidiar con esto (a diferencia de la solución en github).
Craig van Tonder
y ya, si está agregando o quitando algo de la matriz original, simplemente llame al método nuevamente y estará listo para continuar con la solución nuevamente.
Viplock
1
_.cloneDeep () realmente ayudó en mi caso. ¡¡Gracias!! ¡De mucha ayuda!
Cristiana Pereira
18

Es un comportamiento bien definido. No puede obtener el valor anterior para un objeto mutado . Eso es porque tanto el newValy se oldValrefieren al mismo objeto. Vue no conservará una copia antigua de un objeto que mutó.

Si hubiera reemplazado el objeto por otro, Vue le habría proporcionado las referencias correctas.

Lea la Notesección en los documentos. ( vm.$watch)

Más sobre esto aquí y aquí .

Capricho
fuente
3
Oh mi sombrero, muchas gracias! Esa es una complicada ... Esperaba completamente que val y oldVal fueran diferentes, pero después de inspeccionarlos, veo que son dos copias de la nueva matriz, no lo rastrea antes. Lea un poco más y encontré esta pregunta SO sin respuesta sobre el mismo malentendido: stackoverflow.com/questions/35991494/…
Craig van Tonder
5

Esto es lo que uso para observar en profundidad un objeto. Mi requisito era observar los campos secundarios del objeto.

new Vue({
    el: "#myElement",
    data:{
        entity: {
            properties: []
        }
    },
    watch:{
        'entity.properties': {
            handler: function (after, before) {
                // Changes detected.    
            },
            deep: true
        }
    }
});
Alper Ebicoglu
fuente
Creo que es posible que se esté perdiendo la comprensión del cavet que se describió en stackoverflow.com/a/41136186/2110294 . Para que quede claro, esta no es una solución a la pregunta y no funcionará como esperaba en determinadas situaciones.
Craig van Tonder
¡esto es exactamente lo que estaba mirando !. Gracias
Jaydeep Shil
Lo mismo aquí, exactamente lo que necesitaba !! Gracias.
Guntar
4

La solución de componentes y la solución de clonación profunda tienen sus ventajas, pero también presentan problemas:

  1. A veces, desea realizar un seguimiento de los cambios en los datos abstractos; no siempre tiene sentido crear componentes en torno a esos datos.

  2. La clonación profunda de toda su estructura de datos cada vez que realiza un cambio puede resultar muy costosa.

Creo que hay una forma mejor. Si desea ver todos los elementos de una lista y saber qué elemento de la lista cambió, puede configurar observadores personalizados en cada elemento por separado, así:

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

Con esta estructura, handleChange()recibirá el elemento de la lista específico que cambió; desde allí, puede realizar el manejo que desee.

También he documentado un escenario más complejo aquí , en caso de que esté agregando / eliminando elementos a su lista (en lugar de solo manipular los elementos que ya están allí).

Erik Koopmans
fuente
Gracias Erik, presenta puntos válidos y la metodología proporcionada es definitivamente útil si se implementa como una solución a la pregunta.
Craig van Tonder