Vue - Observando profundamente uma série de objetos e calculando a mudança?

108

Eu tenho uma matriz chamada peopleque contém objetos da seguinte maneira:

Antes

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

Pode mudar:

Depois de

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

Observe que Frank acabou de fazer 33 anos.

Tenho um aplicativo em que tento observar a matriz de pessoas e, quando qualquer um dos valores muda, registro a mudança:

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

Baseei isso na pergunta que fiz ontem sobre comparações de array e selecionei a resposta mais rápida.

Então, neste ponto, espero ver um resultado de: { id: 1, name: 'Frank', age: 33 }

Mas tudo o que recebo de volta no console é (tendo em mente que eu o tinha em um componente):

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

E no codepen que fiz , o resultado é um array vazio e não o objeto alterado que mudou, o que seria o que eu esperava.

Se alguém pudesse sugerir por que isso está acontecendo ou onde eu errei aqui, ficaria muito grato, muito obrigado!

Craig van Tonder
fonte

Respostas:

136

Sua função de comparação entre o valor antigo e o novo valor está tendo alguns problemas. É melhor não complicar tanto as coisas, pois isso aumentará seu esforço de depuração posteriormente. Você deve mantê-lo simples.

A melhor maneira é criar um person-componente observar cada pessoa separadamente dentro de seu próprio componente, conforme mostrado abaixo:

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

Veja abaixo um exemplo prático para observar o componente de pessoa interna. Se você quiser lidar com isso no lado dos pais, pode usar $emitpara enviar um evento para cima, contendo o idda pessoa 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
fonte
Essa é realmente uma solução de trabalho, mas não está totalmente de acordo com meu caso de uso. Você vê, na verdade eu tenho o aplicativo e um componente, o componente usa a tabela vue-material e lista os dados com a capacidade de editar os valores em linha. Estou tentando alterar um dos valores, em seguida, verificar o que mudou, então, neste caso, ele realmente compara as matrizes antes e depois para ver a diferença. Posso implementar sua solução para resolver o problema? Na verdade, eu provavelmente poderia fazer isso, mas sinto que estaria trabalhando contra o fluxo do que está disponível a esse respeito dentro do vue-material
Craig van Tonder
2
A propósito, obrigado por nos explicar isso, isso me ajudou a aprender mais sobre a Vue, o que eu aprecio!
Craig van Tonder
Levei um tempo para compreender isso, mas você está absolutamente certo, isso funciona como um encanto e é a maneira correta de fazer as coisas se você quiser evitar confusão e mais problemas :)
Craig van Tonder
1
Eu percebi isso também e tive o mesmo pensamento, mas o que também está contido no objeto é o índice de valor que contém o valor, os getters e setters estão lá, mas em comparação, ele os desconsidera, por falta de um melhor entendimento, acho que sim não avaliar em nenhum protótipo. Uma das outras respostas fornece o motivo pelo qual não funcionaria, é porque newVal e oldVal eram a mesma coisa, é um pouco complicado, mas é algo que foi abordado em alguns lugares, ainda outra resposta fornece uma solução decente para facilitar a criação um objeto imutável para fins de comparação.
Craig van Tonder
1
Em última análise, porém, o seu caminho é mais fácil de compreender à primeira vista e fornece mais flexibilidade em termos do que está disponível quando o valor muda. Isso me ajudou muito a entender os benefícios de mantê-lo simples no Vue, mas fiquei um pouco preso a ele, como você viu na minha outra pergunta. Muito Obrigado! :)
Craig van Tonder
21

Eu mudei a implementação dele para ter seu problema resolvido, fiz um objeto para rastrear as mudanças antigas e compará-lo com isso. Você pode usá-lo para resolver seu problema.

Aqui criei um método, no qual o valor antigo será armazenado em uma variável separada e, que então será usado em um relógio.

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

Veja o codepen atualizado

Viplock
fonte
Então, quando estiver montado, armazene uma cópia dos dados e use-os para comparar com eles. Interessante, mas meu caso de uso seria mais complexo e não tenho certeza de como isso funcionaria ao adicionar e remover objetos do array, @Quirk forneceu bons links para resolver o problema também. Mas eu não sabia que você pode usar vm.$data, obrigado!
Craig van Tonder
sim, e estou atualizando após o relógio também chamando o método novamente, por isso, se você voltar ao valor original, ele também rastreará a alteração.
Viplock
Ohhh, não percebi que esconder aí faz muito sentido e é uma forma menos complicada de lidar com isso (ao contrário da solução no github).
Craig van Tonder
e sim, se você está adicionando ou removendo algo do array original, apenas chame o método novamente e você estará pronto para prosseguir com a solução novamente.
Viplock
1
_.cloneDeep () realmente ajudou no meu caso. Obrigado!! Realmente util!
Cristiana Pereira
18

É um comportamento bem definido. Você não pode obter o valor antigo de um objeto mutado . Isso porque ambos newVale e se oldValreferem ao mesmo objeto. O Vue não manterá uma cópia antiga de um objeto que você modificou.

Se você tivesse substituído o objeto por outro, Vue teria fornecido as referências corretas.

Leia a Noteseção nos documentos. ( vm.$watch)

Mais sobre isso aqui e aqui .

Quirk
fonte
3
Oh meu chapéu, muito obrigado! Essa é uma questão complicada ... Eu esperava completamente que val e oldVal fossem diferentes, mas depois de inspecioná-los, vejo que são duas cópias do novo array, ele não controla isso antes. Leia um pouco mais e encontrei esta pergunta SO sem resposta sobre o mesmo mal-entendido: stackoverflow.com/questions/35991494/…
Craig van Tonder
5

É o que eu uso para observar profundamente um objeto. Minha exigência era observar os campos filho do objeto.

new Vue({
    el: "#myElement",
    data:{
        entity: {
            properties: []
        }
    },
    watch:{
        'entity.properties': {
            handler: function (after, before) {
                // Changes detected.    
            },
            deep: true
        }
    }
});
Alper Ebicoglu
fonte
Eu acredito que você pode estar perdendo o entendimento do cavet que foi descrito em stackoverflow.com/a/41136186/2110294 . Só para ficar claro, esta não é uma solução para a questão e não funcionará como você esperava em certas situações.
Craig van Tonder
isso é exatamente o que eu estava procurando !. Obrigado
Jaydeep Shil
O mesmo aqui, exatamente o que eu precisava !! Obrigado.
Guntar
4

A solução de componente e a solução de clone profundo têm suas vantagens, mas também têm problemas:

  1. Às vezes, você deseja rastrear alterações em dados abstratos - nem sempre faz sentido construir componentes em torno desses dados.

  2. A clonagem profunda de toda a estrutura de dados sempre que você fizer uma alteração pode ser muito cara.

Acho que existe uma maneira melhor. Se você deseja observar todos os itens em uma lista e saber qual item da lista foi alterado, você pode configurar observadores personalizados em cada item separadamente, assim:

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

Com essa estrutura, handleChange()receberá o item específico da lista que mudou - a partir daí você poderá fazer o manuseio que desejar.

Também documentei um cenário mais complexo aqui , no caso de você estar adicionando / removendo itens da sua lista (em vez de apenas manipular os itens já existentes).

Erik Koopmans
fonte
Obrigado Erik, você apresenta pontos válidos e a metodologia fornecida é definitivamente útil se implementada como uma solução para a questão.
Craig van Tonder