Throttle decorator
Creare un “throttling” decorator throttle(f, ms)
– che ritorna un wrapper.
Quando viene chiamato più volte, passa la chiamata a f
al massimo una volta ogni ms
millisecondi.
Rispetto al debounce decorator abbiamo un decorator completamente diverso:
debounce
esegue la funzione una volta, dopo il periodo di “cooldown”. Valido per processare il risultato finale.throttle
la esegue non più spesso dell’intervallo di tempoms
. Valido per aggiornamenti regolari ma non troppo frequenti.
In altre parole, throttle
è come una segretaria che accetta telefonate, ma le passa al capo (chiama f
) non più di una volta ogni ms
millisecondi.
Vediamo l’applicazione nella vita reale, per capire meglio tale esigenza e per vedere da dove nasce.
Ad esempio, vogliamo tenere traccia dei movimenti del mouse.
In un browser possiamo impostare una funzione da eseguire ad ogni movimento del mouse, e ottenere la posizione del puntatore mentre si sposta. Durante un utilizzo attivo del mouse, questa funzione di solito viene eseguita molto frequentemente, può essere qualcosa come 100 volte al secondo (ogni 10 ms).
Vorremmo aggiornare alcune informazioni sulla pagina web quando il puntatore si sposta.
… Ma l’aggiornamento della funzione update()
è troppo pesante per farlo ad ogni micro-movimento. Inoltre, non ha senso aggiornare più spesso di una volta ogni 100 ms.
Quindi la andremo ad inserire nel decorator, usando throttle(update, 100)
come funzione da eseguire ad ogni movimento del mouse, invece dell’originale update()
. Il decorator verrà chiamato spesso, ma inoltrerà la chiamata a update()
al massimo una volta ogni 100 ms.
Visivamente, sarà simile a questo:
- Per il primo movimento del mouse la variante decorata passa immediatamente la chiamata ad
update
. Questo è importante, l’utente vede immediatamente una reazione al suo movimento. - Successivamente, per i movimenti del mouse entro lo scadere di
100ms
, non accade nulla. La variante decorata ignora le chiamate. - Allo scadere dei
100ms
viene chiamato un ulterioreupdate
con le ultime coordinate. - Infine, il mouse si ferma da qualche parte. La variante decorata attende la scadenza dei
100ms
e poi esegueupdate
con le ultime coordinate. Quindi, cosa abbastanza importante, vengono elaborate le coordinate finali del mouse.
Un esempio del codice:
function f(a) {
console.log(a);
}
// f1000 passa ad f un massimo di una chiamata ogni 1000 ms
let f1000 = throttle(f, 1000);
f1000(1); // visualizza 1
f1000(2); // (throttling, 1000ms non ancora scaduti)
f1000(3); // (throttling, 1000ms non ancora scaduti)
// allo scadere dei 1000 ms...
// ...visualizza 3, il valore intermedio 2 viene ignorato
P.S. Gli argomenti e il contesto this
passati a f1000
dovrebbero essere passati alla funzione f
originale.
function throttle(func, ms) {
let isThrottled = false,
savedArgs,
savedThis;
function wrapper() {
if (isThrottled) { // (2)
savedArgs = arguments;
savedThis = this;
return;
}
isThrottled = true;
func.apply(this, arguments); // (1)
setTimeout(function() {
isThrottled = false; // (3)
if (savedArgs) {
wrapper.apply(savedThis, savedArgs);
savedArgs = savedThis = null;
}
}, ms);
}
return wrapper;
}
La chiamata a throttle(func, ms)
ritorna wrapper
.
- Durante la prima chiamata, il
wrapper
semplicemente eseguefunc
ed imposta lo stato cooldown (isThrottled = true
). - In questo stato, tutte le chiamate vengono memorizzate in
savedArgs/savedThis
. Va notato che sia il contesto che gli argomenti sono ugualmente importanti e dovrebbero essere memorizzati. Ne abbiamo bisogno contemporaneamente per riprodurre la chiamata. - Dopo che sono passati
ms
millisecondi,setTimeout
scatta. Lo stato cooldown viene rimosso (isThrottled = false
) e, nel caso fossero state ignorate delle chiamate,wrapper
viene eseguito con gli ultimi argomenti e contesto memorizzati.
Il terzo passaggio non esegue func
, ma wrapper
, perché non abbiamo bisogno solo di eseguire func
, ma anche di impostare nuovamente lo stato di cooldown ed il timeout per resettarlo.