¿Cómo implementar debounce en Vue2?

143

Tengo un cuadro de entrada simple en una plantilla Vue y me gustaría usar debounce más o menos así:

<input type="text" v-model="filterKey" debounce="500">

Sin embargo, la debouncepropiedad ha quedado en desuso en Vue 2 . La recomendación solo dice: "use v-on: input + función de rebote de terceros".

¿Cómo lo implementas correctamente?

Intenté implementarlo usando lodash , v-on: input y v-model , pero me pregunto si es posible prescindir de la variable adicional.

En plantilla:

<input type="text" v-on:input="debounceInput" v-model="searchInput">

En script:

data: function () {
  return {
    searchInput: '',
    filterKey: ''
  }
},

methods: {
  debounceInput: _.debounce(function () {
    this.filterKey = this.searchInput;
  }, 500)
}

La clave de filtro se usa luego en computedaccesorios.

MartinTeeVarga
fuente
3
Sugeriría leer cuidadosamente: vuejs.org/v2/guide/…
Marek Urbanowicz
3
Hay un ejemplo en la guía: vuejs.org/v2/guide/computed.html#Watchers
Bengt

Respuestas:

158

Estoy usando el paquete NPM debounce e implementado así:

<input @input="debounceInput">

methods: {
    debounceInput: debounce(function (e) {
      this.$store.dispatch('updateInput', e.target.value)
    }, config.debouncers.default)
}

Usando lodash y el ejemplo en la pregunta, la implementación se ve así:

<input v-on:input="debounceInput">

methods: {
  debounceInput: _.debounce(function (e) {
    this.filterKey = e.target.value;
  }, 500)
}
Primoz Rome
fuente
10
Gracias por esto. Encontré un ejemplo similar en algunos otros documentos de Vue: vuejs.org/v2/examples/index.html (el editor de rebajas)
MartinTeeVarga
55
La solución propuesta tiene un problema cuando hay varias instancias de componentes en la página. El problema se describe y la solución se presenta aquí: forum.vuejs.org/t/issues-with-vuejs-component-and-debounce/7224/…
Valera
e.currentTarget se sobrescribe como nulo de esta manera
ness-EE
1
Recomendaría agregar un v-model=your_input_variable a la entrada y en su vue data. Por lo tanto, no confía e.targetsino que usa Vue para poder acceder en this.your_input_variablelugar dee.target.value
DominikAngerer
1
Para aquellos que usan ES6, es importante enfatizar el uso de la función anónima aquí: si usa una función de flecha, no podrá acceder this dentro de la función.
Polosson
68

Asignar un rebote methodspuede ser un problema. Entonces, en lugar de esto:

// Bad
methods: {
  foo: _.debounce(function(){}, 1000)
}

Puedes probar:

// Good
created () {
  this.foo = _.debounce(function(){}, 1000);
}

Se convierte en un problema si tiene varias instancias de un componente, de forma similar a como datadebería ser una función que devuelve un objeto. Cada instancia necesita su propia función antirrebote si se supone que debe actuar de forma independiente.

Aquí hay un ejemplo del problema:

Vue.component('counter', {
  template: '<div>{{ i }}</div>',
  data: function(){
    return { i: 0 };
  },
  methods: {
    // DON'T DO THIS
    increment: _.debounce(function(){
      this.i += 1;
    }, 1000)
  }
});


new Vue({
  el: '#app',
  mounted () {
    this.$refs.counter1.increment();
    this.$refs.counter2.increment();
  }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js"></script>

<div id="app">
  <div>Both should change from 0 to 1:</div>
  <counter ref="counter1"></counter>
  <counter ref="counter2"></counter>
</div>

bendytree
fuente
1
¿Podría explicar por qué asignar problemas de rebote puede ser un problema?
MartinTeeVarga
12
Ver ejemplos Los enlaces son propensos a la rotura de enlaces. Es mejor explicar el problema en la respuesta, lo hará más valioso para los lectores.
MartinTeeVarga
Muchas gracias coincidir, que tenía un mal tiempo tratando de entender por qué los datos que aparecen en la consola fue correcta, pero no se aplica en la aplicación ...
@ sm4 porque, en lugar de utilizar la misma instancia compartida sin rebote para la función deseada, la recrea cada vez, lo que elimina el uso de rebote principalmente.
Mike Sheward el
1
solo agrégalo a tu data()entonces.
Su-Au Hwang
45

actualizado en 2020

Opción 1: reutilizable, sin profundidad

(Recomendado si es necesario más de una vez en su proyecto)

helpers.js

export function debounce (fn, delay) {
  var timeoutID = null
  return function () {
    clearTimeout(timeoutID)
    var args = arguments
    var that = this
    timeoutID = setTimeout(function () {
      fn.apply(that, args)
    }, delay)
  }
}

Componente.vue

<script>
  import {debounce} from './helpers'

  export default {
    data () {
      return {
        input: '',
        debouncedInput: ''
      }
    },
    watch: {
      input: debounce(function (newVal) {
        this.debouncedInput = newVal
      }, 500)
    }
  }
</script>

Codepen


Opción 2: en componentes, sin profundidad

(Recomendado si se usa una vez o en un proyecto pequeño)

Componente.vue

<template>
    <input type="text" v-model="input" />
</template>

<script>
  export default {
    data: {
      debouncedInput: ''
    },
    computed: {
     input: {
        get() {
          return this.debouncedInput
        },
        set(val) {
          if (this.timeout) clearTimeout(this.timeout)
          this.timeout = setTimeout(() => {
            this.debouncedInput = val
          }, 300)
        }
      }
    }
  }
</script>

Codepen

cavar
fuente
44
eres el verdadero héroe
Ashtonian
44
Prefiero esta opción porque probablemente no necesito un paquete npm para 11 líneas de código ...
Ben Winding
3
Esta debería ser la respuesta marcada, funciona muy bien y casi no ocupa espacio. ¡Gracias!
Alexander Kludt
29

Muy simple sin lodash

  handleScroll: function() {
   if (this.timeout) clearTimeout(this.timeout); 
   this.timeout = setTimeout(() => {
     // your action
   }, 200);
  }
pshx
fuente
44
Por mucho que me encante lodash, esta es claramente la mejor respuesta para un rechazo final. Más fácil de implementar y comprender.
Michael Hays
2
También es bueno agregar destroyed() { clearInterval(this.timeout) }para no tener un tiempo de espera después de la destrucción.
pikilon
13

Tuve el mismo problema y aquí hay una solución que funciona sin complementos.

Dado que <input v-model="xxxx">es exactamente lo mismo que

<input
   v-bind:value="xxxx"
   v-on:input="xxxx = $event.target.value"
>

(fuente)

Pensé que podría establecer una función antirrebote en la asignación de xxxx en xxxx = $event.target.value

Me gusta esto

<input
   v-bind:value="xxxx"
   v-on:input="debounceSearch($event.target.value)"
>

métodos:

debounceSearch(val){
  if(search_timeout) clearTimeout(search_timeout);
  var that=this;
  search_timeout = setTimeout(function() {
    that.xxxx = val; 
  }, 400);
},
stallingOne
fuente
1
Si el campo de entrada también tuvo una @input="update_something"acción a continuación, llamar a esto despuésthat.xxx = val that.update_something();
Neon22
1
En mi sección de métodos, utilicé una sintaxis ligeramente diferente que funcionó para mí:debounceSearch: function(val) { if (this.search_timeout) clearTimeout(this.search_timeout); var that=this; this.search_timeout = setTimeout(function() { that.thread_count = val; that.update_something(); }, 500); },
Neon22
Esto está bien si está teniendo una o muy pocas instancias en las que necesita cancelar la entrada. Sin embargo, se dará cuenta rápidamente de que necesitará mover esto a una biblioteca o similar si la aplicación crece y esta funcionalidad es necesaria en otros lugares. Mantenga su código SECO.
Coreus
5

Tenga en cuenta que publiqué esta respuesta antes de la respuesta aceptada. No es correcto. Es solo un paso adelante de la solución en la pregunta. He editado la pregunta aceptada para mostrar tanto la implementación del autor como la implementación final que había utilizado.


Según los comentarios y el documento de migración vinculado , he realizado algunos cambios en el código:

En plantilla:

<input type="text" v-on:input="debounceInput" v-model="searchInput">

En script:

watch: {
  searchInput: function () {
    this.debounceInput();
  }
},

Y el método que establece la clave de filtro permanece igual:

methods: {
  debounceInput: _.debounce(function () {
    this.filterKey = this.searchInput;
  }, 500)
}

Parece que hay una llamada menos (solo la v-modely no la v-on:input).

MartinTeeVarga
fuente
¿No llamaría esto debounceInput()dos veces por cada cambio? v-on:detectará los cambios de entrada y llamará a rebote, Y debido a que el modelo está vinculado, la función de observación de searchInput TAMBIÉN llamará debounceInput... ¿verdad?
mix3d
@ mix3d No considere esta respuesta. Era solo mi investigación que no quería plantear en la pregunta. Es muy probable que tengas razón. Verifique la respuesta aceptada. Es correcto y lo edité para que coincida con la pregunta.
MartinTeeVarga
Mi error ... No me di cuenta de que había respondido su propia pregunta, ¡ja!
mix3d
5

Si necesita un enfoque muy minimalista para esto, hice uno (originalmente bifurcado de vuejs-tips para que también sea compatible con IE) que está disponible aquí: https://www.npmjs.com/package/v-debounce

Uso:

<input v-model.lazy="term" v-debounce="delay" placeholder="Search for something" />

Luego en su componente:

<script>
export default {
  name: 'example',
  data () {
    return {
      delay: 1000,
      term: '',
    }
  },
  watch: {
    term () {
      // Do something with search term after it debounced
      console.log(`Search term changed to ${this.term}`)
    }
  },
  directives: {
    debounce
  }
}
</script>
Coreus
fuente
Probablemente esta debería ser la solución aceptada, con más de 100 votos. El OP solicitó una solución compacta como esta, y desacopla muy bien la lógica de rebote.
Barney
1

En caso de que necesite aplicar un retraso dinámico con la debouncefunción lodash :

props: {
  delay: String
},

data: () => ({
  search: null
}),

created () {
     this.valueChanged = debounce(function (event) {
      // Here you have access to `this`
      this.makeAPIrequest(event.target.value)
    }.bind(this), this.delay)

},

methods: {
  makeAPIrequest (newVal) {
    // ...
  }
}

Y la plantilla:

<template>
  //...

   <input type="text" v-model="search" @input="valueChanged" />

  //...
</template>

NOTA: en el ejemplo anterior, hice un ejemplo de entrada de búsqueda que puede llamar a la API con un retraso personalizado que se proporciona enprops

roli roli
fuente
1

Aunque casi todas las respuestas aquí ya son correctas, si alguien está buscando una solución rápida, tengo una directiva para esto. https://www.npmjs.com/package/vue-lazy-input

Se aplica a @input y v-model, admite componentes personalizados y elementos DOM, antirrebote y acelerador.

Vue.use(VueLazyInput)
  new Vue({
    el: '#app', 
    data() {
      return {
        val: 42
      }
    },
    methods:{
      onLazyInput(e){
        console.log(e.target.value)
      }
    }
  })
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://unpkg.com/lodash/lodash.min.js"></script><!-- dependency -->
<script src="https://unpkg.com/vue-lazy-input@latest"></script> 

<div id="app">
  <input type="range" v-model="val" @input="onLazyInput" v-lazy-input /> {{val}}
</div>

undefinederror
fuente
0

Si está usando Vue, también puede usarlo en v.model.lazylugar de debouncepero recuerdev.model.lazy que no siempre funcionará, ya que Vue lo limita para componentes personalizados.

Para componentes personalizados debe usar :valuejunto con@change.native

<b-input :value="data" @change.native="data = $event.target.value" ></b-input>

Amir Khadem
fuente
0

Si pudieras mover la ejecución de la función antirrebote a algún método de clase, podrías usar un decorador de utils-decorators lib ( npm install --save utils-decorators):

import {debounce} from 'utils-decorators';

class SomeService {

  @debounce(500)
  getData(params) {
  }
}
vlio20
fuente
-1

Podemos hacerlo usando pocas líneas de código JS:

if(typeof window.LIT !== 'undefined') {
      clearTimeout(window.LIT);
}

window.LIT = setTimeout(() => this.updateTable(), 1000);

¡Solución simple! Trabajo perfecto! Espero que sea útil para ustedes.

tanvir993
fuente
2
Claro ... si quieres contaminar el espacio global y hacerlo de modo que solo 1 elemento pueda usarlo a la vez. Esta es una respuesta terrible.
Desarrollador web híbrido
-1
 public debChannel = debounce((key) => this.remoteMethodChannelName(key), 200)

decorador de propiedades vue

Mayxxp
fuente
2
¿Podría agregar más información sobre esta solución?
rocha
2
Por favor elabore un poco más. Además, tenga en cuenta que este es un hilo viejo con respuestas bien establecidas, entonces, ¿puede aclarar cómo su solución es más adecuada para el problema?
jpnadas
Ayuda más si proporciona una explicación de por qué esta es la solución preferida y explica cómo funciona. Queremos educar, no solo proporcionar código.
El hombre de hojalata