Como escrever um serviço de debounce no AngularJS

A biblioteca de sublinhado fornece uma function de debounce que evita várias chamadas para uma function dentro de um período de tempo definido. Sua versão faz uso de setTimeout.

Como poderíamos fazer isso em código AngularJS puro?

Além disso, podemos fazer uso do estilo $ q promete recuperar o valor de retorno da function chamada após o período de debounce?

Aqui está um exemplo prático de tal serviço: http://plnkr.co/edit/fJwRER?p=preview . Cria um object diferido $q que será resolvido quando a function debitada for finalmente chamada.

Cada vez que a function debounce é chamada, a promise para a próxima chamada da function interna é retornada.

 // Create an AngularJS service called debounce app.factory('debounce', ['$timeout','$q', function($timeout, $q) { // The service is actually this function, which we call with the func // that should be debounced and how long to wait in between calls return function debounce(func, wait, immediate) { var timeout; // Create a deferred object that will be resolved when we need to // actually call the func var deferred = $q.defer(); return function() { var context = this, args = arguments; var later = function() { timeout = null; if(!immediate) { deferred.resolve(func.apply(context, args)); deferred = $q.defer(); } }; var callNow = immediate && !timeout; if ( timeout ) { $timeout.cancel(timeout); } timeout = $timeout(later, wait); if (callNow) { deferred.resolve(func.apply(context,args)); deferred = $q.defer(); } return deferred.promise; }; }; }]); 

Você obtém o valor de retorno da function debounced usando o método then na promise.

 $scope.addMsg = function(msg) { console.log('addMsg called with', msg); return msg; }; $scope.addMsgDebounced = debounce($scope.addMsg, 2000, false); $scope.logReturn = function(msg) { console.log('logReturn called with', msg); var promise = $scope.addMsgDebounced(msg); promise.then(function(msg) { console.log('Promise resolved with', msg); }); }; 

Se você chamar logReturn várias vezes em uma sucessão rápida, verá a chamada logReturn sendo repetida, mas apenas uma chamada addMsg registrada.

Angular 1.3 debounce como padrão

Vale a pena mencionar que o debounce vem incorporado com o Angular 1.3. Como seria de esperar, é implementado como uma diretiva. Você consegue fazer isso:

  

O atributo $ scope.address não é atualizado até 500 ms após o último pressionamento de tecla.

Se você precisar de mais controle

Se você quiser mais granularidade, poderá definir diferentes tempos de devolução para diferentes events:

  

Aqui, por exemplo, temos um debounce de 500 ms por um toque de tecla e nenhum debounce por um borrão.

Documentação

Leia a documentação aqui: https://docs.angularjs.org/api/ng/directive/ngModelOptions

Desde que eu escrevi os comentários acima, eu tive uma mudança de opinião sobre isso.

A resposta curta é que você não precisa debitar funções que retornam valores.

Por quê? Bem, filosoficamente, acho que faz mais sentido continuar a debater para events e apenas para events. Se você tiver um método que retorne um valor que gostaria de debitar, deverá, em vez disso, debounce o evento que faz com que seu método seja executado downstream.

Pete BD deu um bom começo para o serviço de debounce, no entanto, vejo dois problemas:

  1. retorna quando você deve enviar um retorno de chamada work () que use o fechamento de javascript se você precisar alterar o estado no chamador.
  2. variável de tempo limite – essa variável de tempo limite não é um problema? timeout [] talvez? imagine 2 diretivas usando debounce – signalr, validador de formulário de input, eu acredito que a abordagem de fábrica seria quebrada.

O que estou usando atualmente:

Eu mudei a fábrica para um serviço, então cada diretiva recebe uma nova instância de debounce aka new instance da variável timeout. – Eu não corri para uma situação em que uma diretiva vai precisar de tempo limite para ser timeout [].

 .service('reactService', ['$timeout', '$q', function ($timeout, $q) { this.Debounce = function () { var timeout; this.Invoke = function (func, wait, immediate) { var context = this, args = arguments; var later = function () { timeout = null; if (!immediate) { func.apply(context, args); } }; var callNow = immediate && !timeout; if (timeout) { $timeout.cancel(timeout); } timeout = $timeout(later, wait); if (callNow) { func.apply(context, args); } }; return this; } }]); 

no meu validador remoto angularjs

  .directive('remoteValidator', ['$http', 'reactService', function ($http, reactService) { return { require: 'ngModel', link: function (scope, elm, attrs, ctrl) { var newDebounce = new reactService.Debounce(); var work = function(){ //.... }; elm.on('blur keyup change', function () { newDebounce.Invoke(function(){ scope.$apply(work); }, 1000, false); }); } }; }]) 

Há uma boa implementação de um serviço de debounce e uma diretiva que pode funcionar com qualquer modelo ng em: https://github.com/shahata/angular-debounce

Ou simplesmente instale-o usando:

 bower install ng-debounce 

https://github.com/capaj/ng-tools/blob/master/src/debounce.js

uso:

 app.directive('autosavable', function(debounce) { return { restrict : 'A', require : '?ngModel', link : function(scope, element, attrs, ngModel) { var debounced = debounce(function() { scope.$broadcast('autoSave'); }, 5000, false); element.bind('keypress', function(e) { debounced(); }); } }; }); 

O suporte para isso caiu em # 1.3.0.beta6 angularjs se você está lidando com uma interação de modelo.

https://docs.angularjs.org/api/ng/directive/ngModelOptions