VueJS 2 debounce on multiple components

3.7k views Asked by At

I have a Vue component that uses multiple sub-components on it. And on those sub-components I have a watcher that watches for data changes and processes those changes. I would like to implement debounce for this.

    watch: {
    data: {
      handler: function () {
        this.processData()
      },
      deep: true
    }
  },
  methods: {
    processData: debounce(function () {
      console.log(this.id)
    }, 250),

The problem is that debounce works so it executes only on the last sub-component.

I have found a solution for debounce function that accepts an additional id debounceWithId

However there problem is that if I specify this function as follows:

  methods: {
    processData: debounceWithId(function () {
      console.log(this.id)
    }, 250, this.id),

the last this.id is undefined.

What would be a correct way of using debounce in multiple components so the function fires separately on each component?

2

There are 2 answers

6
Bert On BEST ANSWER

First let me add an example that replicates the issue you are describing.

console.clear()

function debounce(func, wait, immediate) {
    var timeout;
    return function() {
        console.log("Called from component ", this._uid)
        var context = this, args = arguments;
        var later = function() {
            timeout = null;
            if (!immediate) func.apply(context, args);
        };
        var callNow = immediate && !timeout;
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
        if (callNow) func.apply(context, args);
    };
};

Vue.component("doesntwork",{
  props:["value"],
  template:`<div>Component #{{_uid}} Value: {{innerValue}}</div>`,
  data(){
    return {
      innerValue: this.value
    }
  },
  watch:{
    value(newVal){
      this.processData(newVal)
    }
  },
  methods:{
    processData: debounce(function(newVal){
      this.innerValue = newVal
    }, 1000)
  },
})


new Vue({
  el: "#app",
  data:{
    parentValue: null,
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<div id="app">
  Type some text. Wait one second. Only the *last* component is updated.<br>
  <input type="text" v-model="parentValue">
  <doesntwork :value="parentValue"></doesntwork>
  <doesntwork :value="parentValue"></doesntwork>
  <doesntwork :value="parentValue"></doesntwork>
</div>

Essentially what is going on here is that the debounced function is created when the component is compiled, and each instance of the component shares the same debounced function. The context of this will be different in each one, but it is the same function. I added a console.log in the generated debounced function so that you can see that all three components are sharing the same function. That being the case, the function is doing what it is designed to do; it executes once after the elapsed period of time, which is why only the last component is updated.

To get around that behavior you need a unique debounce function for each component. Here are two ways you can do that.

Method One

You can initialize your processData method with what amounts to a placeholder.

methods: {
  processData(){}
}

Then, in the created lifecycle event, change the processData method to the debounced method.

created(){
  this.processData = debounce(function(){
    console.log(this.id)
  }, 250)
}

This will give each component a unique debounced function and should take care of the issue where only the last component works properly.

Here is an example modified from the above example.

console.clear()

Vue.component("works",{
  props:["value"],
  template:`<div>Component #{{_uid}} Value: {{innerValue}}</div>`,
  data(){
    return {
      innerValue: this.value,
    }
  },
  watch:{
    value(newVal){
      this.processData(newVal)
    }
  },
  methods:{
    processData() {}
  },
  created(){
    this.processData = _.debounce(function(newVal){
      this.innerValue = newVal
    }, 1000)
  }
})

new Vue({
  el: "#app",
  data:{
    parentValue: null,
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<div id="app">
  Type some text. Wait one second. <em>All</em> components are updated.<br>
  <input type="text" v-model="parentValue">
  <works :value="parentValue"></works>
  <works :value="parentValue"></works>
  <works :value="parentValue"></works>
</div>

Method Two

Thanks to @RoyJ for suggesting this. You can define the processData method in data. Typically you do not do this because you don't often want multiple copies of a function, and that's why the methods section of a component definition exists, but in a case, like this where you need a unique function for each component, you can define the method in the data function because the data function is called for every instance of the component.

data(){
  return {
    innerValue: this.value,
    processData: _.debounce(function(newVal){
      this.innerValue = newVal
    }, 1000)
  }
},

Here is an example using that approach.

console.clear()

Vue.component("works",{
  props:["value"],
  template:`<div>Component #{{_uid}} Value: {{innerValue}}</div>`,
  data(){
    return {
      innerValue: this.value,
      processData: _.debounce(function(newVal){
        this.innerValue = newVal
      }, 1000)
    }
  },
  watch:{
    value(newVal){
      this.processData(newVal)
    }
  },
})

new Vue({
  el: "#app",
  data:{
    parentValue: null,
  }
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://unpkg.com/[email protected]"></script>
<div id="app">
  Type some text. Wait one second. <em>All</em> components are updated.<br>
  <input type="text" v-model="parentValue">
  <works :value="parentValue"></works>
  <works :value="parentValue"></works>
  <works :value="parentValue"></works>
</div>

0
HuyDD On

another solution that works for me is using $watch api to add a watcher after the component is created then the debounce function will not be shared between components

created () {
  this.$watch(
    'foo',
    debounce(function bar() {
      // do something
    },
    {}
  )
}