Vue watcher triggered on creating sibling property

Viewed 80

I'm experiencing problems with vue watchers.

How to explain such behavior: watcher on test.a is triggered on creating test.b property?

vm = new Vue({
  data: {
    test: { a: {}, c: {} }
  }
})
vm.$watch('test.a', () => {console.log('> test.a changed')})
vm.$watch('test.b', () => {console.log('> test.b changed')})
vm.$watch(() => vm.test.c, () => {console.log('> test.c changed')}, {deep: true})

setTimeout(() => {
  console.log('creating: test.b')
  Vue.set(vm.test, 'b', {})
}, 10)

setTimeout(() => {
  console.log('changing: test.b')
  vm.test.b = 1
}, 20)
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

EDIT:

In real world I use deep watchers, but such behavior was observed for both: normal and deep watcher types.

Creating new property triggers all sibbling deep watches and in most cases normal watches.

2 Answers

Vue.set(vm.test, 'b', {}) this is triggering a change in test just once.

   vm = new Vue({
      data: {
        test: { a: {}, b: {}}
      }
    })
    vm.$watch('test.a', () => {console.log('> test.a changed', vm.test)}, {deep: true})
    vm.$watch('test.b', () => {console.log('> test.b changed', vm.test.b)}, {deep: true})
    setTimeout(() => {
  Vue.set(vm.test, 'b', {name: 1})
    }, 1000)
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>

using deep: true and test: {a: {}, b: {}} should solve the problem

One solution is to use a function that returns the JSON of the nested object, instead of using a dot-delimited path:

vm.$watch(
  // The first function returns the JSON representation of the object
  () => JSON.stringify(vm.test.a),

  // Handler
  () => console.log('> test.a changed')
);

However, for the case of vm.test.b, which may be undefined at runtime, then you will need to use the new null-coalescing operator ?? that is supported by some modern browsers:

vm.$watch(
  // The first function returns the JSON representation of the object
  () => JSON.stringify(vm.test.b ?? {}),

  // Handler
  () => console.log('> test.a changed')
);

If you want backwards compatibility, you can use a more verbose check, i.e.:

vm.$watch(
  // The first function returns the JSON representation of the object
  () => JSON.stringify(vm.test.b === void 0 ? {} : vm.test.b),

  // Handler
  () => console.log('> test.a changed')
);

vm = new Vue({
  data: {
    test: {
      a: {}
    }
  }
})
vm.$watch(
  () => JSON.stringify(vm.test.a),
  () => console.log('> test.a changed')
);
vm.$watch(
  () => JSON.stringify(vm.test.b === void 0 ? {} : vm.test.b),
  () => console.log('> test.b changed')
);

setTimeout(() => {
  console.log('creating: test.b')
  Vue.set(vm.test, 'b', {})
}, 10)

setTimeout(() => {
  console.log('changing: test.b')
  vm.test.b = 1
}, 20)
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.11/vue.min.js"></script>

Related