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 и перечисляет данные с возможностью редактирования значений в режиме реального времени. Я пытаюсь изменить одно из значений, а затем проверяю, что изменилось, поэтому в этом случае он действительно сравнивает массивы до и после, чтобы увидеть, какая разница. Могу я реализовать ваше решение для решения проблемы? В самом деле, я мог бы сделать это, но просто чувствую, что это будет работать против потока того, что доступно в этом отношении в vue-material
Крейг ван Тондер

2
Кстати, спасибо, что нашли время объяснить это, это помогло мне узнать больше о Vue, что я очень ценю!
Craig van Tonder

Мне потребовалось время, чтобы осознать это, но вы абсолютно правы, это работает как шарм и является правильным способом сделать что-то, если вы хотите избежать путаницы и дальнейших проблем :)
Крейг ван Тондер

1
Я тоже это заметил, и у меня была такая же мысль, но то, что также содержится в объекте, - это индекс значения, который содержит значение, есть геттеры и сеттеры, но для сравнения он их игнорирует, из-за отсутствия лучшего понимания, я думаю, что да не оцениваю ни на каких прототипах. Один из других ответов объясняет причину, по которой это не сработает, потому что newVal и oldVal были одним и тем же, это немного сложно, но это то, что было решено в нескольких местах, еще один ответ обеспечивает достойную работу для легкого создания неизменяемый объект для сравнения.
Craig van Tonder

1
В конечном счете, ваш путь легче понять с первого взгляда и обеспечивает большую гибкость с точки зрения того, что доступно при изменении значения. Это очень помогло мне понять преимущества простоты во Vue, но я немного зациклился на этом, как вы видели в другом моем вопросе. Большое спасибо! :)
Craig van Tonder

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, спасибо!
Craig van Tonder

да, и я обновляю его после часов, также снова вызывая метод, с его помощью, если вы вернетесь к исходному значению, тогда он также будет отслеживать изменение.
Viplock

Оооо, я не заметил, что прятаться там имеет смысл и это менее сложный способ справиться с этим (в отличие от решения на github).
Craig van Tonder

и да, если вы добавляете или удаляете что-то из исходного массива, просто вызовите метод еще раз, и вы снова можете использовать решение.
Viplock

1
_.cloneDeep () очень помог в моем случае. Спасибо!! Действительно полезно!
Cristiana Pereira

18

Это четко определенное поведение. Вы не можете получить старое значение для измененного объекта. Это потому, что и newValи oldValотносятся к одному и тому же объекту. Vue не будет хранить старую копию объекта, который вы изменили.

Если бы вы заменили объект другим, Vue предоставил бы вам правильные ссылки.

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

Подробнее об этом здесь и здесь .


3
Шляпа моя, большое спасибо! Это сложно ... Я полностью ожидал, что val и oldVal будут разными, но после их проверки я вижу, что это две копии нового массива, он не отслеживал их раньше. Прочтите еще немного и обнаружите этот вопрос без ответа SO относительно того же недоразумения: stackoverflow.com/questions/35991494/…
Craig van Tonder

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.