Vue - глибоке перегляд масиву об'єктів та обчислення змін?


108

У мене є масив, який називається, peopleякий містить такі об'єкти:

До цього

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

Це може змінитися:

Після

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

Зауважте, Френку щойно виповнилося 33 роки.

У мене є додаток, де я намагаюся спостерігати масив людей, і коли будь-яке значення змінюється, тоді записую зміни:

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

Я ґрунтував це на питанні, яке я задав учора про порівняння масиву, і вибрав найшвидшу робочу відповідь.

Отже, в цей момент я очікую результат: { id: 1, name: 'Frank', age: 33 }

Але все, що я повертаю в консоль, - це (маючи на увазі, що я мав це в складі):

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

І в кодені, який я зробив , результат - порожній масив, а не змінений об'єкт, який змінився, і це було б те, чого я очікував.

Якщо хтось міг би підказати, чому це відбувається або де я тут помилився, то це було б дуже вдячно, велике спасибі!

Відповіді:


136

Ваша функція порівняння між старим значенням і новим значенням викликає певні проблеми. Краще не сильно ускладнювати речі, оскільки це згодом збільшить ваші зусилля налагодження. Ви повинні тримати це просто.

Найкращий спосіб - створити person-componentта переглядати кожну людину окремо всередині її компонента, як показано нижче:

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

Нижче наведено робочий приклад для перегляду компонента всередині людини. Якщо ви хочете обробити це на батьківській стороні, ви можете використовувати $emitдля надсилання події вгору, що містить idзмінену особу.

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>


Це дійсно робоче рішення, але це не зовсім відповідно до мого випадку використання. Розумієте, насправді у мене є додаток і один компонент, компонент використовує таблицю vue-material та перераховує дані з можливістю редагувати значення в рядку. Я намагаюся змінити одне із значень, потім перевіряю, що змінилося, щоб у цьому випадку він насправді порівнював масиви до і після, щоб побачити, у чому різниця. Чи можу я реалізувати ваше рішення для вирішення проблеми? Дійсно, я міг би зробити це, але просто відчуваю, що це буде працювати проти потоку того, що доступно в цьому плані, в межах матеріалу
Крейг ван Тондер

2
До речі, дякую, що знайшов час, щоб пояснити це, це допомогло мені дізнатися більше про Vue, що я дуже ціную!
Крейг ван Тондер

Щоб зрозуміти це, мені знадобилося певний час, але ви абсолютно праві, це працює як принадність, і це правильний спосіб робити речі, якщо ви хочете уникнути плутанини та подальших проблем :)
Крейг ван Тондер

1
Я теж помітив це і мав таку ж думку, але те, що також міститься в об'єкті, - це індекс значення, який містить значення, геттери та сетери є, але в порівнянні він їх ігнорує, через відсутність кращого розуміння, я думаю, що це робить не оцінювати за жодними прототипами. Один з інших відповідей дає причину, чому вона не працюватиме, це тому, що newVal і oldVal були одне і те ж, це трохи складніше, але це щось, що вирішено в декількох місцях, а інша відповідь забезпечує гідну роботу навколо для легкого створення непорушний об’єкт для цілей порівняння.
Крейг ван Тондер

1
Зрештою, ваш спосіб простіше зрозуміти з першого погляду та забезпечує більшу гнучкість у плані того, що доступно, коли значення змінюється. Це мені дуже допомогло зрозуміти переваги простоти у Vue, але трохи застряг у ній, як ви бачили в іншому моєму запитанні. Велике дякую! :)
Крейг ван Тондер

21

Я змінив її реалізацію, щоб вирішити вашу проблему, я створив об'єкт для відстеження старих змін і порівняння з цим. Ви можете використовувати його для вирішення своєї проблеми.

Тут я створив метод, в якому старе значення буде зберігатися в окремій змінній і, яке потім буде використовуватися в годиннику.

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

Дивіться оновлений коден


Тож коли він встановлений, зберігайте копію даних і використовуйте це для порівняння з ними. Цікаво, але мій варіант використання був би складнішим, і я не впевнений, як це буде працювати при додаванні та видаленні об’єктів з масиву, @Quirk надав хороші посилання для вирішення проблеми. Але я не знав, що ти можеш використовувати vm.$data, дякую!
Крейг ван Тондер

так, і я оновлюю його після перегляду, також зателефонувавши знову за методом, якщо ви повернетесь до початкового значення, то також він буде відслідковувати зміни.
Виплок

Ох, я не помітив, що ховатися там має багато сенсу і є менш складним способом боротьби з цим (на відміну від рішення на Github).
Крейг ван Тондер

і ya, якщо ви додаєте або видаляєте щось із початкового масиву, просто зателефонуйте знову до методу, і ви знову готові до вирішення проблеми.
Виплок

1
_.cloneDeep () дійсно допоміг у моєму випадку. Дякую!! Дуже корисно!
Кріштіана Перейра

18

Це чітко визначена поведінка. Ви не можете отримати старе значення мутованого об'єкта. Це тому, що обидва newValі oldValпосилаються на один і той же об'єкт. Vue не збереже стару копію предмета, який ви мутували.

Якби ви замінили об'єкт іншим, Vue надав би вам правильні посилання.

Прочитайте Noteрозділ у документах. ( vm.$watch)

Детальніше про це тут і тут .


3
О мій капелюх, спасибі велике! Це хитро ... Я повністю очікував, що вал і oldVal будуть різними, але після огляду їх я бачу, що це дві копії нового масиву, він не відстежував цього раніше. Прочитайте ще трохи і знайдіть це не відповідне питання щодо того ж непорозуміння: stackoverflow.com/questions/35991494/…
Крейг ван Тондер

5

Це те, що я використовую для глибокого перегляду предмета. Моєю вимогою було спостереження за дочірніми полями об’єкта.

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

Я вважаю, що вам може не вистачити розуміння порожнини, описаної в stackoverflow.com/a/41136186/2110294 . Щоб було зрозуміло, це не вирішення питання і не спрацює так, як ви, можливо, очікували в певних ситуаціях.
Крейг ван Тондер

це саме те, що я шукав !. Спасибі
Jaydeep Shil

Те саме тут, саме те, що мені було потрібно !! Дякую.
Гунтар

4

Компонентне рішення та глибоко-клоновий розчин мають свої переваги, але також мають проблеми:

  1. Іноді потрібно відстежувати зміни в абстрактних даних - це не завжди має сенс будувати компоненти навколо цих даних.

  2. Поглиблене клонування всієї вашої структури даних кожного разу, коли ви вносите зміни, може бути дуже дорогим.

Я думаю, що є кращий спосіб. Якщо ви хочете переглянути всі елементи в списку і знати, який елемент у списку змінився, ви можете встановити спеціальні спостерігачі на кожен елемент окремо, наприклад:

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

Завдяки цій структурі handleChange()ви отримаєте певний елемент списку, який змінився - звідти ви можете виконувати будь-яку обробку, яка вам подобається.

Тут я також задокументував складніший сценарій , якщо ви додаєте / видаляєте елементи до свого списку (а не лише маніпулюєте предметами, які вже є).


Дякую Еріку, ви представляєте дійсні моменти, і надана методологія є, безумовно, корисною, якщо вона буде реалізована як рішення питання.
Крейг ван Тондер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.