JavaScript bietet außergewöhnliche Flexibilität beim Umgang mit Funktionen. Sie können herumgereicht und als Objekte verwendet werden. Jetzt werden wir sehen, wie man Anrufe zwischen ihnen weiterleitet und wie man sie dekoriert .
Nehmen wir an, wir haben eine Funktion slow(x)
die CPU-lastig ist, deren Ergebnisse aber stabil sind. Mit anderen Worten: Für dasselbe x
wird immer das gleiche Ergebnis zurückgegeben.
Wenn die Funktion häufig aufgerufen wird, möchten wir möglicherweise die Ergebnisse zwischenspeichern (merken), um zu vermeiden, dass zusätzliche Zeit für Neuberechnungen aufgewendet wird.
Aber anstatt diese Funktionalität in slow()
hinzuzufügen, erstellen wir eine Wrapper-Funktion, die Caching hinzufügt. Wie wir sehen werden, bietet dies viele Vorteile.
Hier ist der Code und es folgen Erklärungen:
Funktion langsam(x) { // Hier kann es zu einer sehr CPU-intensiven Aufgabe kommen alarm(`Aufgerufen mit ${x}`); x zurückgeben; } Funktion cachingDecorator(func) { let cache = new Map(); Rückgabefunktion(x) { if (cache.has(x)) { // ob ein solcher Schlüssel im Cache vorhanden ist return Cache.get(x); // das Ergebnis daraus lesen } let result = func(x); // sonst rufe func auf Cache.set(x, Ergebnis); // und das Ergebnis zwischenspeichern (merken). Ergebnis zurückgeben; }; } slow = cachingDecorator(slow); alarm( langsam(1) ); // slow(1) wird zwischengespeichert und das Ergebnis zurückgegeben alarm( "Noch einmal: " + slow(1) ); // slow(1) vom Cache zurückgegebenes Ergebnis alarm( langsam(2) ); // slow(2) wird zwischengespeichert und das Ergebnis zurückgegeben alarm( "Noch einmal: " + slow(2) ); // slow(2) vom Cache zurückgegebenes Ergebnis
Im obigen Code ist cachingDecorator
ein Dekorator : eine spezielle Funktion, die eine andere Funktion annimmt und deren Verhalten ändert.
Die Idee ist, dass wir cachingDecorator
für jede Funktion aufrufen können und der Caching-Wrapper zurückgegeben wird. Das ist großartig, denn wir können viele Funktionen haben, die eine solche Funktion nutzen könnten, und alles, was wir tun müssen, ist cachingDecorator
auf sie anzuwenden.
Durch die Trennung des Cachings vom Hauptfunktionscode halten wir auch den Hauptcode einfacher.
Das Ergebnis von cachingDecorator(func)
ist ein „Wrapper“: function(x)
der den Aufruf von func(x)
in die Caching-Logik „umhüllt“:
Von einem externen Code aus macht die Wrapped- slow
-Funktion immer noch dasselbe. Es wurde lediglich ein Caching-Aspekt zu seinem Verhalten hinzugefügt.
Zusammenfassend lässt sich sagen, dass die Verwendung eines separaten cachingDecorator
mehrere Vorteile bietet, anstatt den Code von slow
selbst zu ändern:
Der cachingDecorator
ist wiederverwendbar. Wir können es auf eine andere Funktion anwenden.
Die Caching-Logik ist separat und hat die Komplexität von slow
selbst (falls vorhanden) nicht erhöht.
Bei Bedarf können wir mehrere Dekorateure kombinieren (weitere Dekorateure folgen).
Der oben erwähnte Caching-Dekorator ist nicht für die Arbeit mit Objektmethoden geeignet.
Im folgenden Code funktioniert beispielsweise worker.slow()
nach der Dekoration nicht mehr:
// Wir machen worker.slow zum Caching let worker = { someMethod() { Rückgabe 1; }, langsam(x) { // gruselige CPU-lastige Aufgabe hier alarm("Aufgerufen mit " + x); return x * this.someMethod(); // (*) } }; // gleicher Code wie zuvor Funktion cachingDecorator(func) { let cache = new Map(); Rückgabefunktion(x) { if (cache.has(x)) { return Cache.get(x); } let result = func(x); // (**) Cache.set(x, Ergebnis); Ergebnis zurückgeben; }; } alarm( worker.slow(1) ); // Die ursprüngliche Methode funktioniert worker.slow = cachingDecorator(worker.slow); // Jetzt zwischenspeichern alarm( worker.slow(2) ); // Hoppla! Fehler: Die Eigenschaft „someMethod“ von undefiniert kann nicht gelesen werden
Der Fehler tritt in der Zeile (*)
auf, die versucht, auf this.someMethod
zuzugreifen, was jedoch fehlschlägt. Können Sie verstehen, warum?
Der Grund dafür ist, dass der Wrapper die ursprüngliche Funktion als func(x)
in der Zeile (**)
aufruft. Und wenn die Funktion so aufgerufen wird, erhält sie this = undefined
.
Wir würden ein ähnliches Symptom beobachten, wenn wir versuchen würden, Folgendes auszuführen:
let func = worker.slow; func(2);
Der Wrapper übergibt den Aufruf also an die ursprüngliche Methode, jedoch ohne den Kontext this
. Daher der Fehler.
Lass es uns reparieren.
Es gibt eine spezielle integrierte Funktionsmethode func.call(context, …args), die es ermöglicht, eine Funktion aufzurufen, die this
explizit festlegt.
Die Syntax lautet:
func.call(context, arg1, arg2, ...)
Es führt func
aus und stellt das erste Argument als this
und das nächste als Argumente bereit.
Vereinfacht ausgedrückt bewirken diese beiden Aufrufe fast dasselbe:
func(1, 2, 3); func.call(obj, 1, 2, 3)
Beide rufen func
mit den Argumenten 1
, 2
und 3
auf. Der einzige Unterschied besteht darin, dass func.call
this
auch auf obj
setzt.
Als Beispiel rufen wir im folgenden Code sayHi
im Kontext verschiedener Objekte auf: sayHi.call(user)
führt sayHi
aus und stellt this=user
bereit, und die nächste Zeile legt this=admin
fest:
Funktion sayHi() { alarm(this.name); } let user = { name: "John" }; let admin = { name: "Admin" }; // Aufruf verwenden, um verschiedene Objekte als „this“ zu übergeben sayHi.call( user ); // John sayHi.call( admin ); // Admin
Und hier verwenden wir call
, um say
mit dem gegebenen Kontext und der angegebenen Phrase aufzurufen:
Funktion say(phrase) { Alert(this.name + ': ' + Phrase); } let user = { name: "John" }; // user wird zu diesem und „Hello“ wird zum ersten Argument say.call( user, „Hallo“ ); // John: Hallo
In unserem Fall können wir call
im Wrapper verwenden, um den Kontext an die ursprüngliche Funktion zu übergeben:
let worker = { someMethod() { Rückgabe 1; }, langsam(x) { alarm("Aufgerufen mit " + x); return x * this.someMethod(); // (*) } }; Funktion cachingDecorator(func) { let cache = new Map(); Rückgabefunktion(x) { if (cache.has(x)) { return Cache.get(x); } let result = func.call(this, x); // „this“ wird jetzt korrekt übergeben Cache.set(x, Ergebnis); Ergebnis zurückgeben; }; } worker.slow = cachingDecorator(worker.slow); // Machen Sie es jetzt zum Caching alarm( worker.slow(2) ); // funktioniert alarm( worker.slow(2) ); // funktioniert, ruft nicht das Original auf (zwischengespeichert)
Jetzt ist alles in Ordnung.
Um alles klarer zu machen, schauen wir uns genauer an, wie this
weitergegeben wird:
Nach der Dekoration worker.slow
kommt nun die Wrapper function (x) { ... }
.
Wenn also worker.slow(2)
ausgeführt wird, erhält der Wrapper 2
als Argument und this=worker
(es ist das Objekt vor dem Punkt).
Vorausgesetzt, das Ergebnis ist noch nicht zwischengespeichert, übergibt func.call(this, x)
innerhalb des Wrappers das aktuelle this
( =worker
) und das aktuelle Argument ( =2
) an die ursprüngliche Methode.
Jetzt machen wir cachingDecorator
noch universeller. Bisher funktionierte es nur mit Einzelargumentfunktionen.
Wie kann nun die Methode worker.slow
mit mehreren Argumenten zwischengespeichert werden?
let worker = { langsam(min, max) { Rückgabe min + max; // Es wird davon ausgegangen, dass es zu einer gruseligen CPU-Belastung kommt } }; // sollte sich Aufrufe mit demselben Argument merken worker.slow = cachingDecorator(worker.slow);
Bisher konnten wir für ein einzelnes Argument x
einfach cache.set(x, result)
zum Speichern des Ergebnisses und cache.get(x)
zum Abrufen verwenden. Aber jetzt müssen wir uns das Ergebnis für eine Kombination von Argumenten (min,max)
merken. Die native Map
verwendet nur einen einzelnen Wert als Schlüssel.
Es sind viele Lösungen möglich:
Implementieren Sie eine neue kartenähnliche Datenstruktur (oder verwenden Sie eine Drittanbieter-Datenstruktur), die vielseitiger ist und mehrere Schlüssel ermöglicht.
Verwenden Sie verschachtelte Karten: cache.set(min)
ist eine Map
, die das Paar (max, result)
speichert. Wir können also result
als cache.get(min).get(max)
erhalten.
Verbinde zwei Werte zu einem. In unserem speziellen Fall können wir einfach eine Zeichenfolge "min,max"
als Map
Schlüssel verwenden. Aus Gründen der Flexibilität können wir dem Dekorateur die Bereitstellung einer Hashing-Funktion ermöglichen, die weiß, wie aus vielen Werten ein Wert erstellt werden kann.
Für viele praktische Anwendungen ist die 3. Variante gut genug, daher bleiben wir dabei.
Außerdem müssen wir nicht nur x
, sondern alle Argumente in func.call
übergeben. Erinnern wir uns daran, dass wir in einer function()
ein Pseudo-Array ihrer Argumente als arguments
erhalten können, daher sollte func.call(this, x)
durch func.call(this, ...arguments)
ersetzt werden.
Hier ist ein leistungsfähigerer cachingDecorator
:
let worker = { langsam(min, max) { Alert(`Aufgerufen mit ${min},${max}`); Rückgabe min + max; } }; Funktion cachingDecorator(func, hash) { let cache = new Map(); Rückgabefunktion() { let key = hash(argumente); // (*) if (cache.has(key)) { return cache.get(key); } let result = func.call(this, ...arguments); // (**) Cache.set(Schlüssel, Ergebnis); Ergebnis zurückgeben; }; } Funktion hash(args) { return args[0] + ',' + args[1]; } worker.slow = cachingDecorator(worker.slow, hash); alarm( worker.slow(3, 5) ); // funktioniert alarm( "Again " + worker.slow(3, 5) ); // gleich (zwischengespeichert)
Jetzt funktioniert es mit einer beliebigen Anzahl von Argumenten (obwohl auch die Hash-Funktion angepasst werden müsste, um eine beliebige Anzahl von Argumenten zuzulassen. Eine interessante Möglichkeit, damit umzugehen, wird weiter unten behandelt).
Es gibt zwei Änderungen:
In der Zeile (*)
wird hash
aufgerufen, um aus arguments
einen einzelnen Schlüssel zu erstellen. Hier verwenden wir eine einfache „Joining“-Funktion, die die Argumente (3, 5)
in den Schlüssel "3,5"
umwandelt. Komplexere Fälle erfordern möglicherweise andere Hashing-Funktionen.
Dann verwendet (**)
func.call(this, ...arguments)
um sowohl den Kontext als auch alle Argumente, die der Wrapper erhalten hat (nicht nur das erste), an die ursprüngliche Funktion zu übergeben.
Anstelle von func.call(this, ...arguments)
könnten wir func.apply(this, arguments)
verwenden.
Die Syntax der integrierten Methode func.apply lautet:
func.apply(Kontext, Argumente)
Es führt die func
aus, indem es this=context
einstellt und ein Array-ähnliches Objekt args
als Liste der Argumente verwendet.
Der einzige syntaktische Unterschied zwischen call
und apply
besteht darin, dass call
eine Liste von Argumenten erwartet, während apply
ein Array-ähnliches Objekt mitnimmt.
Diese beiden Aufrufe sind also fast gleichwertig:
func.call(context, ...args); func.apply(context, args);
Sie führen denselben Aufruf von func
mit gegebenem Kontext und gegebenen Argumenten aus.
Es gibt nur einen subtilen Unterschied in Bezug auf args
:
Die Spread-Syntax ...
ermöglicht die Übergabe iterierbarer args
als call
Liste.
Das apply
akzeptiert nur arrayartige args
.
…Und für Objekte, die sowohl iterierbar als auch arrayartig sind, wie zum Beispiel ein echtes Array, können wir jedes davon verwenden, aber apply
wird wahrscheinlich schneller sein, da die meisten JavaScript-Engines es intern besser optimieren.
Die Übergabe aller Argumente zusammen mit dem Kontext an eine andere Funktion wird als Anrufweiterleitung bezeichnet.
Das ist die einfachste Form davon:
let wrapper = function() { return func.apply(this, arguments); };
Wenn ein externer Code einen solchen wrapper
aufruft, ist er nicht vom Aufruf der ursprünglichen Funktion func
zu unterscheiden.
Nehmen wir nun noch eine kleine Verbesserung an der Hashing-Funktion vor:
Funktion hash(args) { return args[0] + ',' + args[1]; }
Derzeit funktioniert es nur mit zwei Argumenten. Es wäre besser, wenn es eine beliebige Anzahl von args
zusammenfügen könnte.
Die natürliche Lösung wäre die Verwendung der arr.join-Methode:
Funktion hash(args) { return args.join(); }
…Leider wird das nicht funktionieren. Weil wir hash(arguments)
aufrufen und arguments
Objekt sowohl iterierbar als auch arrayartig ist, aber kein echtes Array.
Der Aufruf join
würde also fehlschlagen, wie wir unten sehen können:
Funktion hash() { alarm( arguments.join() ); // Fehler: arguments.join ist keine Funktion } Hash(1, 2);
Dennoch gibt es eine einfache Möglichkeit, Array-Join zu verwenden:
Funktion hash() { alarm( [].join.call(arguments) ); // 1,2 } Hash(1, 2);
Der Trick heißt Method Borrowing .
Wir übernehmen (leihen) eine Join-Methode aus einem regulären Array ( [].join
) und verwenden [].join.call
um sie im Kontext von arguments
auszuführen.
Warum funktioniert es?
Das liegt daran, dass der interne Algorithmus der nativen Methode arr.join(glue)
sehr einfach ist.
Aus der Spezifikation übernommen, fast „wie sie ist“:
Sei glue
das erste Argument oder, wenn keine Argumente vorhanden sind, ein Komma ","
.
result
sei eine leere Zeichenfolge.
Hängen Sie this[0]
an result
an.
glue
anbringen und this[1]
.
glue
auftragen und this[2]
.
…Machen Sie dies so lange, bis die Elemente this.length
verklebt sind.
result
zurückgeben.
Technisch gesehen nimmt es also this
und fügt this[0]
, this[1]
usw. zusammen. Es ist absichtlich so geschrieben, dass jedes Array wie this
möglich ist (kein Zufall, viele Methoden folgen dieser Praxis). Deshalb funktioniert es auch mit this=arguments
.
Im Allgemeinen ist es sicher, eine Funktion oder Methode durch eine dekorierte zu ersetzen, bis auf eine Kleinigkeit. Wenn die ursprüngliche Funktion Eigenschaften hatte, wie func.calledCount
oder was auch immer, dann stellt die dekorierte Funktion diese nicht bereit. Denn das ist ein Wrapper. Man muss also vorsichtig sein, wenn man sie verwendet.
Wenn beispielsweise im obigen Beispiel die slow
Funktion irgendwelche Eigenschaften hatte, dann ist cachingDecorator(slow)
ein Wrapper ohne diese.
Einige Dekorateure stellen möglicherweise ihre eigenen Eigenschaften zur Verfügung. Beispielsweise kann ein Dekorateur zählen, wie oft eine Funktion aufgerufen wurde und wie viel Zeit dafür gedauert hat, und diese Informationen über Wrapper-Eigenschaften offenlegen.
Es gibt eine Möglichkeit, Dekoratoren zu erstellen, die den Zugriff auf Funktionseigenschaften behalten. Dies erfordert jedoch die Verwendung eines speziellen Proxy
Objekts zum Umschließen einer Funktion. Wir werden es später im Artikel Proxy und Reflect besprechen.
Decorator ist ein Wrapper um eine Funktion, der ihr Verhalten ändert. Die Hauptaufgabe wird weiterhin von der Funktion wahrgenommen.
Dekoratoren können als „Features“ oder „Aspekte“ betrachtet werden, die einer Funktion hinzugefügt werden können. Wir können einen oder mehrere hinzufügen. Und das alles, ohne den Code zu ändern!
Um cachingDecorator
zu implementieren, haben wir folgende Methoden untersucht:
func.call(context, arg1, arg2…) – ruft func
mit gegebenem Kontext und Argumenten auf.
func.apply(context, args) – ruft func
auf und übergibt context
als this
und Array-ähnliche args
in eine Liste von Argumenten.
Die generische Anrufweiterleitung erfolgt normalerweise mit apply
:
let wrapper = function() { return original.apply(this, arguments); };
Wir haben auch ein Beispiel für das Ausleihen von Methoden gesehen, wenn wir eine Methode von einem Objekt übernehmen und sie im Kontext eines anderen Objekts call
. Es ist durchaus üblich, Array-Methoden zu verwenden und sie auf arguments
anzuwenden. Die Alternative besteht darin, ein Restparameterobjekt zu verwenden, das ein echtes Array ist.
Dort gibt es in freier Wildbahn viele Dekorateure. Überprüfen Sie, wie gut Sie diese erreicht haben, indem Sie die Aufgaben dieses Kapitels lösen.
Wichtigkeit: 5
Erstellen Sie einen Decorator spy(func)
, der einen Wrapper zurückgeben soll, der alle Funktionsaufrufe in seiner calls
Eigenschaft speichert.
Jeder Aufruf wird als Array von Argumenten gespeichert.
Zum Beispiel:
Funktion work(a, b) { alarm( a + b ); // Arbeit ist eine beliebige Funktion oder Methode } Arbeit = Spion(Arbeit); Arbeit(1, 2); // 3 Arbeit(4, 5); // 9 for (lassen Sie die Argumente von work.calls) { alarm( 'call:' + args.join() ); // "call:1,2", "call:4,5" }
PS: Dieser Dekorator ist manchmal für Unit-Tests nützlich. Seine erweiterte Form ist sinon.spy
in der Sinon.JS-Bibliothek.
Öffnen Sie eine Sandbox mit Tests.
Der von spy(f)
zurückgegebene Wrapper sollte alle Argumente speichern und dann f.apply
verwenden, um den Aufruf weiterzuleiten.
Funktion spy(func) { function wrapper(...args) { // ...args anstelle von Argumenten verwenden, um ein „echtes“ Array in wrapper.calls zu speichern wrapper.calls.push(args); return func.apply(this, args); } wrapper.calls = []; Rückumschlag; }
Öffnen Sie die Lösung mit Tests in einer Sandbox.
Wichtigkeit: 5
Erstellen Sie eine delay(f, ms)
die jeden Aufruf von f
um ms
Millisekunden verzögert.
Zum Beispiel:
Funktion f(x) { alarm(x); } // Wrapper erstellen sei f1000 = Verzögerung(f, 1000); sei f1500 = Verzögerung(f, 1500); f1000("test"); // zeigt „test“ nach 1000ms an f1500("test"); // zeigt „test“ nach 1500ms an
Mit anderen Worten, delay(f, ms)
gibt eine „um ms
verzögerte“ Variante von f
zurück.
Im obigen Code ist f
eine Funktion eines einzelnen Arguments, aber Ihre Lösung sollte alle Argumente und den Kontext this
übergeben.
Öffnen Sie eine Sandbox mit Tests.
Die Lösung:
Funktion Verzögerung(f, ms) { Rückgabefunktion() { setTimeout(() => f.apply(this, arguments), ms); }; } sei f1000 = Verzögerung(Alarm, 1000); f1000("test"); // zeigt „test“ nach 1000ms an
Bitte beachten Sie, wie hier eine Pfeilfunktion verwendet wird. Wie wir wissen, verfügen Pfeilfunktionen nicht über eigene this
und arguments
, daher übernimmt f.apply(this, arguments)
this
und arguments
aus dem Wrapper.
Wenn wir eine reguläre Funktion übergeben, würde setTimeout
sie ohne Argumente und this=window
aufrufen (vorausgesetzt, wir befinden uns im Browser).
Wir können this
immer noch mithilfe einer Zwischenvariablen übergeben, aber das ist etwas umständlicher:
Funktion Verzögerung(f, ms) { return function(...args) { let saveThis = this; // speichere dies in einer Zwischenvariablen setTimeout(function() { f.apply(savedThis, args); // hier verwenden }, MS); }; }
Öffnen Sie die Lösung mit Tests in einer Sandbox.
Wichtigkeit: 5
Das Ergebnis des debounce(f, ms)
-Dekorators ist ein Wrapper, der Aufrufe von f
anhält, bis ms
Millisekunden Inaktivität vorliegen (keine Aufrufe, „Abklingzeit“), und dann f
einmal mit den neuesten Argumenten aufruft.
Mit anderen Worten: debounce
ist wie eine Sekretärin, die „Anrufe“ entgegennimmt und wartet, bis ms
Ruhe herrscht. Und erst dann übermittelt es die neuesten Anrufinformationen an „den Chef“ (ruft den eigentlichen f
an).
Zum Beispiel hatten wir eine Funktion f
und ersetzten sie durch f = debounce(f, 1000)
.
Wenn dann die umschlossene Funktion bei 0 ms, 200 ms und 500 ms aufgerufen wird und es dann keine Aufrufe gibt, wird das tatsächliche f
nur einmal bei 1500 ms aufgerufen. Das heißt: nach der Abklingzeit von 1000 ms seit dem letzten Aufruf.
…Und es werden die Argumente des allerletzten Aufrufs abgerufen, andere Aufrufe werden ignoriert.
Hier ist der Code dafür (verwendet den Debounce Decorator aus der Lodash-Bibliothek):
let f = _.debounce(alert, 1000); Fa"); setTimeout( () => f("b"), 200); setTimeout( () => f("c"), 500); // entprellte Funktion wartet 1000 ms nach dem letzten Aufruf und führt dann aus: alarm("c")
Nun ein praktisches Beispiel. Nehmen wir an, der Benutzer gibt etwas ein und wir möchten eine Anfrage an den Server senden, wenn die Eingabe abgeschlossen ist.
Es macht keinen Sinn, die Anfrage für jedes eingegebene Zeichen zu senden. Stattdessen möchten wir warten und dann das gesamte Ergebnis verarbeiten.
In einem Webbrowser können wir einen Event-Handler einrichten – eine Funktion, die bei jeder Änderung eines Eingabefelds aufgerufen wird. Normalerweise wird ein Ereignishandler sehr oft für jeden eingegebenen Schlüssel aufgerufen. Wenn wir es jedoch um 1000 ms debounce
, wird es nur einmal aufgerufen, und zwar 1000 ms nach der letzten Eingabe.
In diesem Live-Beispiel gibt der Handler das Ergebnis in ein Feld unten ein. Probieren Sie es aus:
Sehen? Die zweite Eingabe ruft die entprellte Funktion auf, sodass ihr Inhalt 1000 ms nach der letzten Eingabe verarbeitet wird.
debounce
ist also eine großartige Möglichkeit, eine Abfolge von Ereignissen zu verarbeiten: sei es eine Abfolge von Tastendrücken, Mausbewegungen oder etwas anderes.
Es wartet die angegebene Zeit nach dem letzten Aufruf und führt dann seine Funktion aus, die das Ergebnis verarbeiten kann.
Die Aufgabe besteht darin, debounce
Dekorator zu implementieren.
Hinweis: Das sind nur ein paar Zeilen, wenn Sie darüber nachdenken :)
Öffnen Sie eine Sandbox mit Tests.
Funktion entprellen(func, ms) { Auszeit lassen; Rückgabefunktion() { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, arguments), ms); }; }
Ein Aufruf zum debounce
gibt einen Wrapper zurück. Wenn es aufgerufen wird, plant es den ursprünglichen Funktionsaufruf nach einer bestimmten ms
und bricht die vorherige solche Zeitüberschreitung ab.
Öffnen Sie die Lösung mit Tests in einer Sandbox.
Wichtigkeit: 5
Erstellen Sie einen „drosselnden“ Dekorator „ throttle(f, ms)
“, der einen Wrapper zurückgibt.
Wenn es mehrmals aufgerufen wird, wird der Aufruf höchstens einmal pro ms
Millisekunden an f
weitergeleitet.
Im Vergleich zum Debounce Decorator ist das Verhalten völlig anders:
debounce
führt die Funktion einmal nach der „Abklingzeit“ aus. Gut für die Verarbeitung des Endergebnisses.
throttle
führt es nicht öfter als angegebene ms
Zeit aus. Gut für regelmäßige Updates, die nicht sehr oft erfolgen sollten.
Mit anderen Worten, throttle
ist wie eine Sekretärin, die Telefonanrufe entgegennimmt, aber den Chef (ruft den eigentlichen f
an) nicht öfter als einmal pro ms
Millisekunden stört.
Schauen wir uns die reale Anwendung an, um diese Anforderung besser zu verstehen und zu sehen, woher sie kommt.
Wir wollen zum Beispiel Mausbewegungen verfolgen.
In einem Browser können wir eine Funktion einrichten, die bei jeder Mausbewegung ausgeführt wird und die Position des Zeigers ermittelt, während er sich bewegt. Bei aktiver Mausnutzung wird diese Funktion in der Regel sehr häufig ausgeführt, etwa 100 Mal pro Sekunde (alle 10 ms). Wir möchten einige Informationen auf der Webseite aktualisieren, wenn sich der Zeiger bewegt.
…Aber die Aktualisierung der Funktion update()
ist zu aufwändig, um sie bei jeder Mikrobewegung durchzuführen. Es macht auch keinen Sinn, öfter als einmal pro 100 ms zu aktualisieren.
Also packen wir es in den Dekorator ein: Verwenden Sie throttle(update, 100)
als Funktion, die bei jeder Mausbewegung ausgeführt wird, anstelle der ursprünglichen update()
. Der Dekorator wird häufig aufgerufen, leitet den Aufruf jedoch höchstens einmal pro 100 ms an update()
weiter.
Optisch wird es so aussehen:
Bei der ersten Mausbewegung übergibt die dekorierte Variante sofort den Aufruf an update
. Das ist wichtig, der Nutzer sieht sofort unsere Reaktion auf seinen Umzug.
Wenn sich die Maus dann weiterbewegt, passiert bis 100ms
nichts. Die dekorierte Variante ignoriert Aufrufe.
Am Ende von 100ms
erfolgt eine weitere update
mit den letzten Koordinaten.
Dann bleibt die Maus endlich irgendwo stehen. Die dekorierte Variante wartet, bis 100ms
abgelaufen sind, und führt dann update
mit den letzten Koordinaten aus. Es ist also sehr wichtig, dass die endgültigen Mauskoordinaten verarbeitet werden.
Ein Codebeispiel:
Funktion f(a) { console.log(a); } // f1000 leitet Aufrufe maximal einmal pro 1000 ms an f weiter sei f1000 = Throttle(f, 1000); f1000(1); // zeigt 1 f1000(2); // (Drosselung, 1000 ms noch nicht erreicht) f1000(3); // (Drosselung, 1000 ms noch nicht erreicht) // wenn 1000 ms Zeitüberschreitung... // ...gibt 3 aus, Zwischenwert 2 wurde ignoriert
PS-Argumente und der Kontext, this
an f1000
übergeben wurde, sollten an das ursprüngliche f
übergeben werden.
Öffnen Sie eine Sandbox mit Tests.
Funktion Throttle(func, ms) { sei isThrottled = false, gespeicherteArgs, gespeichertDies; Funktion Wrapper() { if (isThrottled) { // (2) gespeicherteArgs = Argumente; saveThis = this; zurückkehren; } isThrottled = true; func.apply(this, arguments); // (1) setTimeout(function() { isThrottled = false; // (3) if (savedArgs) { wrapper.apply(savedThis, savingArgs); saveArgs = saveThis = null; } }, MS); } Rückumschlag; }
Ein Aufruf von throttle(func, ms)
gibt wrapper
zurück.
Beim ersten Aufruf führt der wrapper
einfach func
aus und legt den Abklingzustand fest ( isThrottled = true
).
In diesem Zustand werden alle Aufrufe in savedArgs/savedThis
gespeichert. Bitte beachten Sie, dass sowohl der Kontext als auch die Argumente gleichermaßen wichtig sind und auswendig gelernt werden sollten. Wir benötigen sie gleichzeitig, um den Anruf zu reproduzieren.
Nach Ablauf von ms
Millisekunden wird setTimeout
ausgelöst. Der Abklingzustand wird entfernt ( isThrottled = false
) und wenn wir Aufrufe ignoriert haben, wird wrapper
mit den zuletzt gespeicherten Argumenten und dem zuletzt gespeicherten Kontext ausgeführt.
Im dritten Schritt wird nicht func
ausgeführt, sondern wrapper
, da wir nicht nur func
ausführen, sondern auch erneut in den Cooldown-Status wechseln und das Timeout einrichten müssen, um ihn zurückzusetzen.
Öffnen Sie die Lösung mit Tests in einer Sandbox.