Vue.js - Como observar corretamente dados aninhados

414

Estou tentando entender como observar corretamente algumas variações de objetos. Eu tenho um componente pai (arquivos .vue) que recebe dados de uma chamada ajax, coloca os dados dentro de um objeto e os usa para renderizar algum componente filho através de uma diretiva v-for, abaixo de uma simplificação da minha implementação:

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

... então <script>tag interna :

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

os objetos de item são assim:

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

Agora, dentro do componente "player" do meu filho, estou tentando observar a variação de propriedade de qualquer item e uso:

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

Funciona, mas parece um pouco complicado para mim e eu queria saber se este é o caminho certo para fazê-lo. Meu objetivo é realizar alguma ação sempre que propmudar ou myArrayobter novos elementos ou alguma variação dentro dos existentes. Qualquer sugestão será apreciada.

Plástico
fonte
9
Basta usar "item.someOtherProp": function (newVal, oldVal){como sugerido por Ron.
Reiner
1
@ Reiner, era exatamente isso que ele estava tentando evitar na pergunta; é exatamente a mesma coisa usando a sintaxe ES5. Na verdade, não há nenhuma sobrecarga adicional em assistir a um computador e é uma técnica útil em várias situações.
craig_h
1
É possível observar uma mudança em tudo keysdentro de um objeto? Exemplo:"item.*": function(val){...}
Charlie

Respostas:

623

Você pode usar um observador profundo para isso:

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

Agora, isso detectará quaisquer alterações nos objetos na itemmatriz e adições à própria matriz (quando usada com o Vue.set ). Aqui está um JSFiddle: http://jsfiddle.net/je2rw3rs/

EDITAR

Se você não deseja assistir a todas as alterações no objeto de nível superior e apenas deseja uma sintaxe menos incômoda para assistir diretamente aos objetos aninhados, basta assistir a um computed:

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

Aqui está o JSFiddle: http://jsfiddle.net/oa07r5fw/

craig_h
fonte
2
dessa maneira, "manipulador" será chamado sempre que qualquer objeto tiver uma variação, meu objetivo é separar os manipuladores para observar se as alterações acontecem em "objeto" ou no myArray em "someOtherProp" separadamente
Plastic
9
Se você deseja apenas observar alterações em objetos aninhados específicos, o que você está fazendo está bem. Se você quer uma sintaxe menos estranho que você poderia assistir a um computedlugar: jsfiddle.net/c52nda7x
craig_h
Exatamente o que eu estava procurando, agora parece muito lógico para mim: criar uma propriedade computada que retorne o suporte aninhado e observe-o. Tnx muito.
Plastic
Eu só quero acrescentar - que a segunda opção não é muito diferente do observador original. Internamente, uma propriedade computada é apenas um observador em todos os objetos / valores referenciados na função que define um valor de dados para o resultado da função. - Então você provavelmente só torna o código um pouco mais complicado com o mesmo efeito.
Falco
22
@ Falco Acabei de descobrir a resposta em um comentário aqui . A peerbolte indicou que isso pode ser feito colocando o nome do relógio entre aspas simples. Eu testei isso e funciona. Então, neste caso, seria watch: { 'item.foo' : function(newVal, oldVal) { // do work here }} bastante liso. Eu amo Vue!
Ron C
436

Outra boa abordagem e um pouco mais elegante é a seguinte:

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

(Aprendi essa abordagem com @peerbolte no comentário aqui )

Ron C
fonte
47
muito mais elegante (não um pouco): P. Isso deve estar na documentação do Vue.js, pois é um caso de uso comum. Estou procurando há horas uma solução, obrigado pela sua resposta.
Claudiu
11
@ symba É interessante que as pessoas não tenham notado que isso é a mesma coisa que o OP pediu para evitar, apenas na ES5sintaxe. Poderíamos, é claro, argumentar que o ES2015 method definitionconteúdo em si não é muito elegante, mas meio que erra o objetivo da pergunta, que é como evitar colocar o nome entre aspas. Eu estou supondo que a maioria das pessoas estão encobrindo a pergunta original, que já contém a resposta, e estão realmente procurando por algo que, como você diz, não é muito bem documentado,
craig_h
1
@craig_h ponto super interessante. Eu estava originalmente procurando por horas para encontrar uma boa maneira de assistir a um objeto aninhado. E o título dessa pergunta foi a pergunta mais próxima que encontrei, mas senti falta de que ele listasse um suporte citado no corpo da pergunta, mas achou as respostas não tão elegantes. Finalmente encontrei a abordagem de proposta citada no comentário de outra pergunta e fiquei tentada a criar uma pergunta apenas para respondê-la como documentação da abordagem de proposta citada no SO. Mas essa pergunta era exatamente qual seria meu título, então adicionei a resposta aqui. Talvez eu devesse ter criado uma nova pergunta.
Ron C
9
@ RonC Esta é uma pergunta popular e sua resposta é exatamente o que muitas pessoas estão procurando, então acho que fica bem aqui. Todos nós temos tempo limitado e a maioria mal lê as perguntas. Portanto, isso certamente não foi uma crítica à resposta que você forneceu, apenas estou sendo um pouco pedante ao apontar que minha resposta original era específica para a resposta. pergunta e não pretendia ser opinativo. Na verdade, eu gosto da abordagem "observar computado", no entanto, muitos preferem a abordagem menos complicada de "citações", que você resumiu sucintamente em sua resposta.
22717 cravel_h
10
A documentação oficial inclui essa abordagem agora
feihcsim 15/02/19
19

Observação profunda do VueJs em objetos filho

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

Como se você deseja assistir a uma propriedade por um tempo e depois desassociá-la?

Ou para assistir a uma propriedade de componente filho da biblioteca?

Você pode usar o "observador dinâmico":

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

O $watchretorna uma função unwatch que irá parar de assistir se for chamado.

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

Além disso, você pode usar a deepopção:

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

Não deixe de dar uma olhada nos documentos

roli roli
fonte
2
De acordo com os documentos , você não deve usar a seta funções para definir um observador: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

Não vendo isso mencionado aqui, mas também é possível usar o vue-property-decoratorpadrão se você estiver estendendo sua Vueclasse.

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

export default class SomeClass extends Vue {
   ...

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

   ...
}
getglad
fonte
5

Outra maneira de acrescentar que eu costumava "hackear" essa solução era fazer isso: configurei um computedvalor separado que simplesmente retornaria o valor do objeto aninhado.

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
fonte
bom;) mas o que com matrizes? :)
WEBCENTER
3

Meu problema com a resposta aceita do uso deep: trueé que, ao observar profundamente uma matriz, não consigo identificar facilmente qual elemento da matriz contém a alteração. A única solução clara que encontrei é esta resposta, que explica como criar um componente para que você possa assistir cada elemento da matriz individualmente.

krubo
fonte
1
Sim, isso é verdade, mas não é para isso que serve um observador profundo (na verdade, você encontrará oldVale os newValdois fazem referência ao mesmo objeto, o mesmo acontece). A intenção de a deep watcheré fazer algo quando os valores mudam, como emitir um evento, fazer uma chamada ajax etc. Caso contrário, você provavelmente deseja um componente, como apontado na resposta de Mani.
18718 craig_h #
Eu tive o mesmo problema de acompanhar as alterações individuais! Encontrei uma solução e a documentei aqui .
Erik Koopmans
3

Rastreando itens alterados individuais em uma lista

Se você deseja assistir a todos os itens de uma lista e saber qual item da lista foi alterado, é possível configurar observadores personalizados em cada item separadamente, da seguinte forma:

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

Se sua lista não for preenchida imediatamente (como na pergunta original), você poderá mover a lógica createdpara onde for necessário, por exemplo, dentro do .then()bloco.

Observando uma lista de alterações

Se a sua lista em si é atualizada para ter itens novos ou removidos, desenvolvi um padrão útil que "superficial" observa a própria lista e observa / desativa dinamicamente itens à medida que a lista muda:

// 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
fonte
0

Nenhuma das respostas para mim estava funcionando. Na verdade, se você deseja assistir a dados aninhados com componentes sendo chamados várias vezes. Então, eles são chamados com diferentes adereços para identificá-los. Por exemplo, <MyComponent chart="chart1"/> <MyComponent chart="chart2"/> Minha solução alternativa é criar uma variável de estado adicional vuex, que eu atualizo manualmente para apontar para a propriedade que foi atualizada pela última vez.

Aqui está um exemplo de implementação do 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
        },
}

Em qualquer função Component para atualizar a loja:

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

Agora em qualquer componente para obter a atualização:

    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
fonte