vue: Ability to disable/not trigger watch handler on data?

For my application, I’m mutating the object in my data: [{...}, {...}, {...}] to state changes occurring at other open instances of my application (happening through web sockets etc, etc). I have a handler on the data structure like this below:

watch: {
    todos: {
        deep: true,
        handler: todoStorage.save,
    },
},

Triggering todoStorage.save would unnecessarily save the contents of the array back to my database where I already know the current state of the application.

Is there a way to mutate the array without triggering the handler? It seems that trying to undefine the handler while making the operation doesn’t work.

About this issue

  • Original URL
  • State: closed
  • Created 9 years ago
  • Reactions: 9
  • Comments: 29 (2 by maintainers)

Most upvoted comments

I made a mixin which brings a $withoutWatchers() method. This method disables the watchers within its callback :

$withoutWatchers(() => {
  // Watchers will not get fired
})

The following is a real world example of a user’s form. We want this component to take the user as a prop, copy it in an internal value, dispatch the changes when this internal value is updated, and update the internal value when the prop changes :

export default {
  data: () => ({
    model: null
  }),

  props: {
    user: {
      type: Object,
      required: true
    }
  },

  created () {
    this.$watch('user', this.sync, {
      immediate: true
    })
  },

  methods: {
    sync () {
      this.$withoutWatchers(() => {
        this.model = { ...this.user }
      })
    }
  },

  watch: {
    model () {
      this.$emit('update:user', this.model)
    }
  }
}
Here is the mixin

const WithoutWatchers = {
  methods: {
    $withoutWatchers (cb) {
      const watchers = this._watchers.map((watcher) => ({ cb: watcher.cb, sync: watcher.sync }))

      for (let index in this._watchers) {
        this._watchers[index] = Object.assign(this._watchers[index], { cb: () => null, sync: true })
      }

      cb()

      for (let index in this._watchers) {
        this._watchers[index] = Object.assign(this._watchers[index], watchers[index])
      }
    }
  }
}

Be aware that this mixin uses the internal Vue’s API, which may change between minor versions. That would be cool to have it within Vue’s core. Could we consider bringing it @yyx990803 ?

It should be pretty obvious at this point that this would be a rather useful feature in many situations.

Ugly:

resetForm() {
    this.workbench.isResetting = true
    value.tier = value.original_tier
    this.nextTick(()=>{
        this.workbench.isResetting = false
    })
}

watch: {
    'value.tier'(new_,old) {
        if (this.workbench.isResetting)
            return;
        //...
    }
}

Nice:

resetForm() {
    Vue.set(value,'tier',value.original_tier, false)
}

Nice >> Ugly

The point of a watcher is that it will fire when the data changes. Instead of thinking about stopping it from firing, just do a conditional check inside the watcher callback.

I’d rather have some kind of source added to the watch. Something along the lines of

<input type="checkbox" id="checkbox" v-source="my-html" v-model="light_is_on">
<label for="checkbox">Kitchen light is: {{ light_is_on ? "on" : "off" }}</label>

watch: {
  light_is_on: function(value, old, source) {
    if (source !== this.websocketHandler) {
      websocket.send({'light_is_on': value})
    }
    if (source == 'my-html') {
      console.log('watch triggered from HTML element')
    }
  },
},
methods: {
  websocketHandler(event) {
    if (event.light_is_on !== undefined) {
      this.light_is_on = event.light_is_on;
    }
  },
},

Hello, i guess i understand the issue from @dalanmiller.

In fact, i was stuck on a similar issue : the fact that you don’t want the watcher trigger the callback every time when you manually decide to affect the value somewhere.

For instance : i got my data from an rest api call in an async way. Then i decide to affect the data in my component, but the watcher detect change and will trigger the callback that notice update the changes to my rest api : there were no data change but i got an api ‘Set’ call ! 😕

More into this issue, i did a codepen to explain that (VueJS 2.0) : http://codepen.io/matyo91/pen/ZBpjVz 📝 What i want is to fire change only when i manually change the value into the textarea. Here, the ‘code’ variable will call onUpdateCode on sync change every time => not what i want 🐛 . And the ‘coder’ variable will not call onUpdateCoder because i did add a dirty silence mechanism. And onUpdateCoder is only called when i do change text into the coder textarea. The coder is what i want 🍎 (i tried a better solution with Vue.nextTick, but it fail, i think it could have worked if Vue.nextTick can dispatch by setting a priority parameter)

I come to this code, because somehow, i got a similar issue when integrating ace.js into a VueJS component. And the ace team got the same issue and it’s resolved by that comment from @nightwing : https://github.com/ajaxorg/ace/issues/503#issuecomment-163761023 The next comment from @davidthornton notice it’s a https://en.wikipedia.org/wiki/Operational_transformation logic.

@yyx990803, you did already answered this in this issue : https://github.com/vuejs/vue/issues/1157 But it’s not really this case here.

So i think it’s more a design pattern issue. But now i don’t know how to write it into a “smart code”.

Any idea ?!

It seems I encounter a similar issue, my solution is that use this.$watch instead of watch option, this.$watch return a handler to unwatch function, which I can call it when I want to stop watch, after that I watch the value again, it’s terrible…

For anyone finding this still using Vue 2, as of 2.7.x abellion’s solution above needs to be changed to:

 export default {
	methods: {
		$withoutWatchers(cb) {
			const watcher = {
				cb: this._watcher.cb,
				sync: this._watcher.sync,
			};

			this._watcher = Object.assign(this._watcher, { cb: () => null, sync: true });

			cb();

			this._watcher = Object.assign(this._watcher, watcher);
		},
	},
};

I like the idea of getting the source from the watch function(value, old, source). I have the same problem and there are only ugly solutions.

Hello, here is what i’m doing for that problem. I had to face with reseting twice search and filters, without trigger multiple watch at the same time.

data: {
  search: null,
  filters: { filter1: null, filter2: null },
  watchInPause: false,
},
watch: {
  search() {
    if (!this.watchInPause) {
      this.refresh();
    },
  },
  filters() {
    handler() {
      if (!this.watchInPause) {
        this.refresh();
      }
    },
    deep: true,
  },
},
async onResetFilters() {
  this.watchInPause = true;
  this.search = null;
  this.filters = {
    filter1: null,
    filter2: null,
  };
  await this.refresh();
  this.watchInPause = false;
},

Hope this helps …

Here’s a follow up to @abellion’s solution.

As abellion predicted, Vue’s internal architecture seems to have changed between versions 2 and 3, and I found that the mixin no longer worked after upgrading.

After a little digging, I found that the watchers had moved within Vue’s hierarchy, but the basic premise appears to be same.

Here’s an updated version of @abellion’s mixin. I believe this works correctly in Vue 3. However I strongly suggest that anyone deciding to adopt it should test it carefully.

export default {
    methods: {
        $withoutWatchers (cb) {
            const watchers = this._.type.watch;

            for (let index in this._.type.watch) {
                this._.type.watch[index] = Object.assign(this._.type.watch[index], { cb: () => null, sync: true })
            }

            cb()

            for (let index in this._.type.watch) {
                this._.type.watch[index] = Object.assign(this._.type.watch[index], watchers[index])
            }
        }
    }
}

whether you use $watch and unwatch, or set a flag to prevent the watch from handling, you’ll need to wait for this.$nextTick() otherwise you will unwatch/turn off the flag prematurely and the watcher will still get executed.

Had the same issue. But I think the fix is simple. Just use the watch function to decide if you would like to call a method:

// vue 1.0
this.$watch('myModel', function(newVal, oldVal) {
    if([your statement]) {
        this.loadSomeData();
    }
});