2009-05-13 19 views
20

Ich habe ein Javascript-Objekt, das 2 Aufrufe an einen externen Server erfordert, um seinen Inhalt zu erstellen und alles sinnvoll zu tun. Das Objekt ist so aufgebaut, dass das Instanziieren einer Instanz automatisch zu diesen 2 Aufrufen führt. Die zwei Aufrufe teilen eine gemeinsame Rückruffunktion, die mit den zurückgegebenen Daten arbeitet und dann eine andere Methode aufruft. Das Problem ist, dass die nächste Methode erst aufgerufen werden sollte, wenn beide Methoden zurückkehren. Hier ist der Code, wie ich es zur Zeit umgesetzt haben:Javascript - Synchronisierung nach asynchronen Aufrufen

foo.bar.Object = function() { 
this.currentCallbacks = 0; 
this.expectedCallbacks = 2; 

this.function1 = function() { 
    // do stuff 
    var me = this; 
    foo.bar.sendRequest(new RequestObject, function(resp) { 
     me.commonCallback(resp); 
     }); 
}; 

this.function2 = function() { 
    // do stuff 
    var me = this; 
    foo.bar.sendRequest(new RequestObject, function(resp) { 
     me.commonCallback(resp); 
     }); 
}; 

this.commonCallback = function(resp) { 
    this.currentCallbacks++; 
    // do stuff 
    if (this.currentCallbacks == this.expectedCallbacks) { 
     // call new method 
    } 
}; 

this.function1(); 
this.function2(); 
} 

Wie Sie sehen können, bin ich gezwungen, das Objekt fortzusetzen, nachdem beide Anrufe zurückgegeben haben einen einfachen Zähler zu validieren sie beide zurück. Das funktioniert aber scheint eine wirklich schlechte Implementierung zu sein. Ich habe erst seit ein paar Wochen mit Javascript gearbeitet und frage mich, ob es eine bessere Methode gibt, das Gleiche zu tun, über das ich noch stolpern muss.

Danke für jede Hilfe.

+0

Die Art, wie Sie es tun, ist richtig. Ich sehe mit Ihrer derzeitigen Methode nichts falsch. Jede Instanz benötigt einen eigenen Zähler, um zu wissen, wann sie alle Antworten erhalten hat. Das ist die einzige Methode, die ich mir vorstellen kann, um Ihr Problem zu lösen. – fearphage

Antwort

5

Es gibt kaum einen anderen Weg, als diesen Zähler zu haben. Eine andere Option wäre, ein Objekt {} zu verwenden und einen Schlüssel für jede Anfrage hinzuzufügen und es zu entfernen, wenn es fertig ist. Auf diese Weise wüssten Sie sofort, was zurückgekehrt ist. Aber die Lösung bleibt gleich.

Sie können den Code ein wenig ändern. Wenn es in Ihrem Beispiel so ist, dass Sie innerhalb von commonCallback nur eine andere Funktion aufrufen müssen (ich nannte es otherFunction), brauchen Sie das commonCallback nicht. Um den Kontext zu speichern, haben Sie bereits Closures verwendet. Statt

foo.bar.sendRequest(new RequestObject, function(resp) { 
      me.commonCallback(resp); 
      }); 

Sie könnte es tun auf diese Weise

foo.bar.sendRequest(new RequestObject, function(resp) { 
      --me.expectedCallbacks || me.otherFunction(resp); 
      }); 
13

Sofern Sie nicht bereit sind, den AJAX zu serialisieren, gibt es keinen anderen Weg, an den ich denken kann, um das zu tun, was Sie vorschlagen. Davon abgesehen, denke ich, was Sie haben, ist ziemlich gut, aber Sie möchten vielleicht die Struktur ein wenig aufräumen, um das Objekt, das Sie mit Initialisierungsdaten erstellen, nicht zu verwerfen.

Hier ist eine Funktion, die Ihnen helfen könnten:

function gate(fn, number_of_calls_before_opening) { 
    return function() { 
     arguments.callee._call_count = (arguments.callee._call_count || 0) + 1; 
     if (arguments.callee._call_count >= number_of_calls_before_opening) 
      fn.apply(null, arguments); 
    }; 
} 

Diese Funktion ist das, was als eine Funktion höherer Ordnung bekannt ist - eine Funktion, die Funktionen als Argumente übernimmt. Diese spezielle Funktion gibt eine Funktion zurück, die die übergebene Funktion aufruft, wenn sie number_of_calls_before_opening mal aufgerufen wurde. Zum Beispiel:

var f = gate(function(arg) { alert(arg); }, 2); 
f('hello'); 
f('world'); // An alert will popup for this call. 

Sie könnten Verwendung dieses als Callback-Methode machen:

foo.bar = function() { 
    var callback = gate(this.method, 2); 
    sendAjax(new Request(), callback); 
    sendAjax(new Request(), callback); 
} 

Den zweiten Rückruf, je nachdem, was es wird sichergestellt, dass method genannt wird. Aber das führt zu einem anderen Problem: Die Funktion gate ruft die übergebene Funktion ohne Kontext auf, was bedeutet, dass sich this auf das globale Objekt bezieht, nicht auf das Objekt, das Sie konstruieren. Es gibt mehrere Möglichkeiten, um dies zu umgehen: Sie können entweder schließen this durch Aliasing es zu me oder self. Oder Sie können eine andere Funktion höherer Ordnung erstellen, die genau das tut.

Hier ist, was der erste Fall würde wie folgt aussehen:

foo.bar = function() { 
    var me = this;   
    var callback = gate(function(a,b,c) { me.method(a,b,c); }, 2); 

    sendAjax(new Request(), callback); 
    sendAjax(new Request(), callback); 
} 

Im letzteren Fall wird die andere Funktion höherer Ordnung in etwa wie folgt sein würde:

function bind_context(context, fn) { 
    return function() { 
     return fn.apply(context, arguments); 
    }; 
} 

Diese Funktion eine Funktion zurückgibt, ruft die übergebene Funktion im übergebenen Kontext auf. Ein Beispiel dafür wäre wie folgt:

var obj = {}; 
var func = function(name) { this.name = name; }; 
var method = bind_context(obj, func); 
method('Your Name!'); 
alert(obj.name); // Your Name! 

Um es in Perspektive, Ihr Code aussehen würde wie folgt:

foo.bar = function() { 
    var callback = gate(bind_context(this, this.method), 2); 

    sendAjax(new Request(), callback); 
    sendAjax(new Request(), callback); 
} 

Auf jeden Fall, wenn Sie diese Refactorings gemacht haben, werden Sie räumte das Objekt auf, das aus all seinen Mitgliedern aufgebaut ist, die nur für die Initialisierung benötigt werden.

8

ich hinzufügen, dass Underscore.js has a nice little helper for this:

eine Version der Funktion erstellt, die nur nach dem ersten ausgeführt werden wird dazu aufgerufen, Zähl mal. Nützlich für die Gruppierung von asynchronen Antworten, wo Sie sicher sein möchten, dass alle asynchronen Anrufe beendet sind, bevor Sie fortfahren.

_.after(count, function) 

Der Code für _after (as-Version 1.5.0):

_.after = function(times, func) { 
    return function() { 
    if (--times < 1) { 
     return func.apply(this, arguments); 
    } 
    }; 
}; 

The license info (as-Version 1.5.0)

+1

_.after (Graf, _.bind (Spaß, das);) – tribalvibes

2

, dass einige gute Sachen ist Mr. Kyle.

Um es ein wenig einfacher zu machen, verwende ich normalerweise eine Start und eine Done-Funktion.
-Die Start Funktion nimmt eine Liste von Funktionen, die ausgeführt werden.
-Die Fertig Funktion wird von den Callbacks Ihrer Funktionen aufgerufen, die Sie an die Startmethode übergeben.
- Zusätzlich können Sie eine Funktion oder eine Liste von Funktionen an die done-Methode übergeben, die ausgeführt wird, wenn der letzte Rückruf abgeschlossen ist.

Die Deklarationen sehen so aus.

var PendingRequests = 0; 
function Start(Requests) { 
    PendingRequests = Requests.length; 
    for (var i = 0; i < Requests.length; i++) 
     Requests[i](); 
}; 
//Called when async responses complete. 
function Done(CompletedEvents) { 
PendingRequests--; 
    if (PendingRequests == 0) { 
     for (var i = 0; i < CompletedEvents.length; i++) 
      CompletedEvents[i](); 
    } 
} 

Hier ist ein einfaches Beispiel mit der Google Maps API.

//Variables 
var originAddress = "*Some address/zip code here*"; //Location A 
var formattedAddress; //Formatted address of Location B 
var distance; //Distance between A and B 
var location; //Location B 

//This is the start function above. Passing an array of two functions defined below. 
Start(new Array(GetPlaceDetails, GetDistances)); 


//This function makes a request to get detailed information on a place. 
//Then callsback with the **GetPlaceDetailsComplete** function 
function GetPlaceDetails() { 
    var request = { 
     reference: location.reference //Google maps reference id 
    }; 
    var PlacesService = new google.maps.places.PlacesService(Map); 
    PlacesService.getDetails(request, GetPlaceDetailsComplete); 
} 

function GetPlaceDetailsComplete(place, status) { 
    if (status == google.maps.places.PlacesServiceStatus.OK) { 
     formattedAddress = place.formatted_address; 
     Done(new Array(PrintDetails)); 
    } 
} 


function GetDistances() { 
    distService = new google.maps.DistanceMatrixService(); 
    distService.getDistanceMatrix(
    { 
     origins: originAddress, 
     destinations: [location.geometry.location], //Location contains lat and lng 
     travelMode: google.maps.TravelMode.DRIVING, 
     unitSystem: google.maps.UnitSystem.IMPERIAL, 
     avoidHighways: false, 
     avoidTolls: false 
    }, GetDistancesComplete); 
} 
function GetDistancesComplete(results, status) { 
    if (status == google.maps.DistanceMatrixStatus.OK) { 
     distance = results[0].distance.text; 
     Done(new Array(PrintDetails)); 
    } 
} 

function PrintDetails() { 
    alert(*Whatever you feel like printing.*); 
} 

Also kurz gesagt, hier, was wir tun, ist
eine Reihe von Funktionen an die -Passing starten Funktion
-Der starten Funktion die Funktionen im Array aufruft und setzt die Anzahl der PendingRequests
-In den Callbacks für unsere anstehenden Anforderungen, rufen wir die Done Funktion -Der Geschehen fu nction nimmt eine Reihe von Funktionen
-Der Fertig Funktion dekrementiert den PendingRequests Zähler
-Wenn ihr sind nicht mehr ausstehenden Anforderungen, rufen wir die an die geben Funktionen Funktion Done

, dass ein einfaches ist, aber practicle Beispiel für das Synchronisieren von Webanrufen. Ich habe versucht, ein Beispiel für etwas, das weit verbreitet ist, zu verwenden, also ging ich mit der Google Maps API. Ich hoffe, dass jemand das nützlich findet.

0

Ein anderer Weg wäre, einen Synchronisationspunkt dank eines Timers zu haben. Es ist nicht schön, aber es hat den Vorteil, dass der Anruf nicht zur nächsten Funktion innerhalb des Rückrufs hinzugefügt werden muss.

Hier ist die Funktion execute_jobs der Einstiegspunkt. Es benötigt eine Liste von Daten, die gleichzeitig ausgeführt werden. Zuerst wird die Anzahl der Jobs festgelegt, die auf die Größe list warten sollen. Dann stellt es einen Timer ein, um auf die Endbedingung zu prüfen (die Zahl fällt auf 0). Und schließlich sendet es für jede Daten einen Job. Jeder Job verringert die Anzahl der erwarteten Jobs um eins.

Es würde aussehen wie etwas wie folgt aus:

var g_numJobs = 0; 

function async_task(data) { 
    // 
    // ... execute the task on the data ... 
    // 

    // Decrease the number of jobs left to execute. 
    --g_numJobs; 
} 

function execute_jobs(list) { 
    // Set the number of jobs we want to wait for. 
    g_numJobs = list.length; 

    // Set the timer (test every 50ms). 
    var timer = setInterval(function() { 
     if(g_numJobs == 0) { 
      clearInterval(timer); 
      do_next_action(); 
     } 
    }, 50); 

    // Send the jobs. 
    for(var i = 0; i < list.length; ++i) { 
     async_task(list[i])); 
    } 
} 

diesen Code verbessern Sie ein Job und JobList Klassen tun. Die Job würde einen Rückruf ausführen und die Anzahl der ausstehenden Jobs verringern, während der JobList den Timer aggregieren und den Callback zur nächsten Aktion aufrufen würde, sobald die Jobs beendet sind.

-1

Ich teilte die gleiche Frustration. Als ich mehr asynchrone Anrufe anlegte, wurde es eine Rückruf-Hölle. Also habe ich meine eigene Lösung gefunden. Ich bin mir sicher, dass es ähnliche Lösungen gibt, aber ich wollte etwas sehr einfaches und benutzerfreundliches erstellen. Asynq ist ein Skript, das ich geschrieben habe, um asynchrone Tasks zu verketten. So f2 nach f1 zu laufen, können Sie tun:

asynq.run (f1, f2)

Sie können Kette so viele Funktionen wie Sie wollen. Sie können auch Parameter angeben oder eine Reihe von Aufgaben für Elemente in einem Array ausführen. Ich hoffe, dass diese Bibliothek Ihre Probleme oder ähnliche Probleme lösen kann, die andere haben.

Verwandte Themen