Skip to content

Commit a48abee

Browse files
add Function.debounce
This method will return a new function that will be called only once per group of close calls. After a defined delay it will be able to be called again.
1 parent 882f2dd commit a48abee

File tree

3 files changed

+305
-1
lines changed

3 files changed

+305
-1
lines changed

Docs/Types/Function.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,56 @@ Executes a function in the specified intervals of time. Periodic execution can b
368368

369369
- [MDN setInterval][], [MDN clearInterval][]
370370

371+
Function: Function.debounce {#Function:Function-debounce}
372+
---------------------------------------------------------
373+
374+
This method will return a new function that will be called only once per group of close calls. After a defined delay it will be able to be called again.
375+
376+
### Syntax:
377+
378+
var debounceFn = myFn.debounce({delay: 100});
379+
380+
### Arguments:
381+
382+
1. obj - (*mixed*) If this argument is a number it will use it as the *delay* timeout of the debounce. If this argument is a object it will allow more specific configuration. Leaving the argument empty will fall to default values.
383+
384+
### Returns:
385+
386+
* (*function*) A debounce function that will be called only once per group of close function calls.
387+
388+
### Examples:
389+
390+
// get scroll position after scroll has stopped
391+
var getNewScrollPosition = function () {
392+
var scroll = window.getScroll();
393+
alert(scroll.y);
394+
}
395+
window.addEvent('scroll', getNewScrollPosition.debounce(500));
396+
397+
// send a Request to server with data from a search field
398+
// 500ms after you stopped typing, to avoid one request per character
399+
var input = document.getElement('input');
400+
var request = new Request({
401+
url: 'your.url',
402+
onSuccess: function(response) {
403+
$('res').set('html', response);
404+
}
405+
});
406+
407+
function fn(){
408+
request.send({data: {value: this.value}});
409+
}
410+
411+
input.addEventListener('keyup', fn.debounce(500));
412+
413+
414+
### Options:
415+
416+
* *delay* - The delay after which the debounce function is called. Defaults to 250ms.
417+
* *when* - When to fire the debounce call. Can be at beginning of group call (*early*), at end (*late*) or *both*. Defaults to *late*.
418+
* *once* - If *true* will call the function only once per event handler. Defaults to *false*.
419+
420+
371421

372422
Deprecated Functions {#Deprecated-Functions}
373423
============================================

Source/Types/Function.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,31 @@ Function.implement({
7272

7373
periodical: function(periodical, bind, args){
7474
return setInterval(this.pass((args == null ? [] : args), bind), periodical);
75+
},
76+
77+
debounce: function(opts){
78+
var timeout,
79+
fn = this,
80+
fireOnce = 0;
81+
if (typeof opts == 'number') opts = {delay: opts};
82+
else opts = opts || {};
83+
if (!opts.delay) opts.delay = 250;
84+
85+
return function(){
86+
if (fireOnce == 2 && opts.when == 'both' || fireOnce && opts.when != 'both') return;
87+
var self = this,
88+
args = arguments,
89+
callNow = !timeout && (opts.when == 'early' || opts.when == 'both');
90+
var later = function(){
91+
if (opts.when != 'early') fn.apply(self, args);
92+
timeout = null;
93+
if (opts.once) fireOnce = 2;
94+
};
95+
clearTimeout(timeout);
96+
timeout = setTimeout(later, opts.delay);
97+
if (callNow) fn.apply(self, args);
98+
if (opts.once && callNow) fireOnce = 1;
99+
};
75100
}
76101

77102
});

Specs/Types/Function.js

Lines changed: 230 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,7 @@ describe('Function.bind', function(){
272272
});
273273

274274
dit('should still be possible to use it as constructor', function(){
275-
function Alien(type) {
275+
function Alien(type){
276276
this.type = type;
277277
}
278278

@@ -459,3 +459,232 @@ describe('Function.periodical', function(){
459459
});
460460

461461
});
462+
463+
describe('Debounce', function(){
464+
var fn, debounceFn, caller, counter = 0,
465+
debounceCalls = 0;
466+
var sum = 0;
467+
468+
function startCaller(){
469+
caller = setInterval(function(){
470+
if (debounceFn){
471+
counter++;
472+
debounceFn();
473+
}
474+
}, 10);
475+
}
476+
beforeEach(function(){
477+
fn = function(){
478+
debounceCalls++;
479+
};
480+
startCaller();
481+
});
482+
483+
afterEach(function(){
484+
expect(counter > 40).toBeTruthy(); // control spec
485+
fn = debounceFn = caller = null;
486+
counter = debounceCalls = 0;
487+
clearInterval(caller);
488+
});
489+
490+
it('should debounce as default', function(){
491+
expect(counter == debounceCalls && debounceCalls == 0).toBeTruthy(); // control spec
492+
debounceFn = fn.debounce();
493+
waitsFor(function(){
494+
if (counter > 40){
495+
clearInterval(caller);
496+
expect(debounceCalls == 0).toBeTruthy();
497+
return true;
498+
}
499+
}, 'enough calls to happen', 1000);
500+
waits(300);
501+
runs(function(){
502+
expect(debounceCalls == 1).toBeTruthy();
503+
});
504+
});
505+
506+
it('should debounce early', function(){
507+
expect(counter == debounceCalls && debounceCalls == 0).toBeTruthy(); // control spec
508+
debounceFn = fn.debounce({
509+
when: 'early',
510+
delay: 150
511+
});
512+
waitsFor(function(){
513+
if (counter > 19){
514+
clearInterval(caller);
515+
caller = null;
516+
expect(debounceCalls == 1).toBeTruthy();
517+
return true;
518+
}
519+
}, 'some calls to happen', 1000);
520+
521+
waits(200);
522+
waitsFor(function(){
523+
if (!caller) startCaller();
524+
if (counter > 40){
525+
clearInterval(caller);
526+
return true;
527+
}
528+
}, 'even more calls to happen', 1000);
529+
waits(200);
530+
runs(function(){
531+
expect(debounceCalls == 2).toBeTruthy();
532+
});
533+
});
534+
535+
it('should debounce once early', function(){
536+
expect(counter == debounceCalls && debounceCalls == 0).toBeTruthy(); // control spec
537+
debounceFn = fn.debounce({
538+
when: 'early',
539+
once: true,
540+
delay: 150
541+
});
542+
waitsFor(function(){
543+
if (counter > 19){
544+
clearInterval(caller);
545+
caller = null;
546+
expect(debounceCalls == 1).toBeTruthy();
547+
return true;
548+
}
549+
}, 'some calls to happen', 1000);
550+
551+
waits(200);
552+
waitsFor(function(){
553+
if (!caller) startCaller();
554+
if (counter > 40){
555+
clearInterval(caller);
556+
return true;
557+
}
558+
}, 'even more calls to happen', 1000);
559+
waits(200);
560+
runs(function(){
561+
expect(debounceCalls == 1).toBeTruthy();
562+
});
563+
});
564+
565+
it('should debounce late', function(){
566+
expect(counter == debounceCalls && debounceCalls == 0).toBeTruthy(); // control spec
567+
debounceFn = fn.debounce(150);
568+
waitsFor(function(){
569+
if (counter > 19){
570+
clearInterval(caller);
571+
caller = null;
572+
expect(debounceCalls == 0).toBeTruthy();
573+
return true;
574+
}
575+
}, 'some calls to happen', 1000);
576+
577+
waits(200);
578+
waitsFor(function(){
579+
if (!caller){
580+
expect(debounceCalls == 1).toBeTruthy();
581+
startCaller();
582+
}
583+
if (counter > 40){
584+
clearInterval(caller);
585+
return true;
586+
}
587+
}, 'even more calls to happen', 1000);
588+
waits(200);
589+
runs(function(){
590+
expect(debounceCalls == 2).toBeTruthy();
591+
});
592+
});
593+
594+
it('should debounce once late', function(){
595+
expect(counter == debounceCalls && debounceCalls == 0).toBeTruthy(); // control spec
596+
debounceFn = fn.debounce({
597+
when: 'late',
598+
once: true
599+
});
600+
waitsFor(function(){
601+
if (counter > 19){
602+
clearInterval(caller);
603+
caller = null;
604+
expect(debounceCalls == 0).toBeTruthy();
605+
return true;
606+
}
607+
}, 'some calls to happen', 1000);
608+
609+
waits(300);
610+
waitsFor(function(){
611+
if (!caller){
612+
expect(debounceCalls == 1).toBeTruthy();
613+
startCaller();
614+
}
615+
if (counter > 40){
616+
clearInterval(caller);
617+
return true;
618+
}
619+
}, 'even more calls to happen', 1000);
620+
waits(300);
621+
runs(function(){
622+
expect(debounceCalls == 1).toBeTruthy();
623+
});
624+
});
625+
626+
it('should debounce both early and late', function(){
627+
expect(counter == debounceCalls && debounceCalls == 0).toBeTruthy(); // control spec
628+
debounceFn = fn.debounce({
629+
when: 'both',
630+
delay: 150
631+
});
632+
waitsFor(function(){
633+
if (counter > 19){
634+
clearInterval(caller);
635+
caller = null;
636+
expect(debounceCalls == 1).toBeTruthy();
637+
return true;
638+
}
639+
}, 'some calls to happen', 1000);
640+
641+
waits(200);
642+
waitsFor(function(){
643+
if (!caller){
644+
expect(debounceCalls == 2).toBeTruthy();
645+
startCaller();
646+
}
647+
if (counter > 40){
648+
clearInterval(caller);
649+
return true;
650+
}
651+
}, 'even more calls to happen', 1000);
652+
waits(200);
653+
runs(function(){
654+
expect(debounceCalls == 4).toBeTruthy();
655+
});
656+
});
657+
658+
it('should debounce both early and late, once', function(){
659+
expect(counter == debounceCalls && debounceCalls == 0).toBeTruthy(); // control spec
660+
debounceFn = fn.debounce({
661+
when: 'both',
662+
once: true
663+
});
664+
waitsFor(function(){
665+
if (counter > 19){
666+
clearInterval(caller);
667+
caller = null;
668+
expect(debounceCalls == 1).toBeTruthy();
669+
return true;
670+
}
671+
}, 'some calls to happen', 1000);
672+
673+
waits(300);
674+
waitsFor(function(){
675+
if (!caller){
676+
expect(debounceCalls == 2).toBeTruthy();
677+
startCaller();
678+
}
679+
if (counter > 40){
680+
clearInterval(caller);
681+
return true;
682+
}
683+
}, 'even more calls to happen', 1000);
684+
waits(300);
685+
runs(function(){
686+
expect(debounceCalls == 2).toBeTruthy();
687+
});
688+
});
689+
690+
});

0 commit comments

Comments
 (0)