Parallele Ajax-Requests mit JQuery Deferred Objects

Problemstellung

Issue Tracker wie JIRA oder YouTrack sind darauf ausgelegt, Aufgaben und Aufwände Projekt-zentriert zu verfolgen und darzustellen. Wir von Flavia als Auftragsentwickler benötigen zusätzlich eine Darstellung, die Nutzer- und Zeit-zentriert aufgebaut ist. Für diese Stundenreports werden die Aufwände (Workitems), die ein Mitarbeiter innerhalb eines bestimmten Zeitraums erbracht hat, über alle Projekte und Aufgaben (Issues) hinweg zusammen gesucht und kompakt dargestellt. Da YouTrack eine solche Darstellung von Haus aus nicht anbietet, wurde unter Nutzung der REST-Schnittstelle eine passende Anwendung entwickelt.

Bei der Implementierung des Stundenreports für YouTrack hat sich schnell herausgestellt, dass sich viele Anfragen an die Dienste parallelisieren lassen. Um Issues eines Projekts und die zugehörigen Workitems zu laden, werden die folgenden REST-Schnittstellen angesprochen:

  • /rest/issue/byproject/{project}
    Holt die Issues eines Projekts
  • /rest/issue/{issue}/timetracking/workitem/
    Holt die Workitems des Issue

YouTrack liefert für Issue-Anfragen standardmäßig 10 Einträge zurück. Die Menge lässt sich mit dem Parameter max einstellen, wenn die Zahl der Issues bekannt ist bzw. die Zahl der Antworten erhöht werden soll. Alternativ können solange Issues abgerufen werden, bis die Zahl der Ergebnisse unter 10 liegt. Natürlich können für jeden erhaltenen 10er-Block bereits Anfragen für die Workitems jedes Issues abgeschickt werden, während parallel dazu weitere Issues angefordert werden. Problematisch hierbei ist, dass nicht bekannt ist, wie viele Anfragen insgesamt gestellt werden müssen. Zudem darf die Anwendung erst zum nächsten Schritt (z.B. der Berechnung der Stunden an einem Tag etc.) weiterlaufen, wenn alle Anfragen beantwortet sind.

Blog - jQuery Deferred

Lösung

Seit jQuery 1.5 gibt es das Deferred Object, mit dessen Hilfe sich asynchrone Prozesse leicht steuern lassen. Das Deferred repräsentiert einen Prozess, der noch nicht abgeschlossen ist (z. B. ein laufender Ajax Call). Das Gegenstück zum Deferred ist das Promise, welches den Wert des Prozesses darstellen soll, sobald dieser beendet ist. Der Aufrufer eines asynchronen Prozesses erhält das Promise-Objekt und seine angegebenen Callback-Funktionen werden ausgeführt, sobald das Ergebnis vorliegt. Dieses Konzept soll nun am folgenden Beispiel verdeutlicht werden:

// for each project call: fetchIssues(project, 0)
function fetchIssues(project, start) {
    var deferred = $.Deferred();
    var query = buildGetIssuesQuery(project.id, start);
    $.ajax(query).done(function(issueData) {
        // process received data ...

        var promise = issues.map(fetchWorkitems);

        if (issueData.length === 10) {
            var nextDef = fetchIssues(project, start + 10);
            promise.push(nextDef);
        }
        $.when.apply($, promise).done(function() {
            deferred.resolve();
        });
    });

    return deferred.promise();
}

Zuerst wird ein Deferred-Objekt erstellt, das die Last der Synchronisation des Asynchronen trägt. Sofort nach dem Ajax-Aufruf an YouTrack (Zeile 5) gibt die Funktion das Promise des Deferred zurück; ein Versprechen, dass Daten geliefert werden. Solange der Caller nicht versucht, auf die konkreten Daten im Promise zuzugreifen, kann mit dem Objekt einfach weitergearbeitet werden.

Die Antwort der Anfrage enthält 10 oder weniger Issues. Bevor sich fetchIssues bei Bedarf in Zeile 11 rekursiv selbst aufruft,  werden für jedes bereits erhaltene Issue die Workitems vom Server abgerufen. Diese asynchronen Aufrufe werden jeweils in der Funktion fetchWorkitems ausgeführt. Das Ergebnis von issues.map(fetchWorkitems)  ist ein Array mit den jqXHR-Objekten der Ajax-Aufrufe. Diese können wie Promise-Objekte behandelt werden, was sich später noch als nützlich erweisen wird.

Wenn in der Server-Antwort 10 Issuess enthalten waren, werden die nächsten Issues abgefragt. Dabei wird bekanntlich wieder ein Promise-Objekt geliefert, welches zu den Promise-Objekten der Workitem-Requests hinzugefügt wird.

Sein Versprechen halten

In Zeile 14 wird es endlich konkret. when()  ist eine jQuery-Funktion, die als beliebig viele Parameter mehrere Promise-Objekte entgegennimmt und ein neues, aggregiertes Promise-Objekt erstellt. Sobald alle übergebenen Promise-Objekte aufgelöst wurden, löst when das erstellte Promise-Objekt auf, was zum Aufruf der angehängten Callback-Funktionen führt (z. B. die Funktionen in done(…) ). Da when kein Array von Promise-Objekte verarbeiten kann, wird der Zwischenschritt mit apply ausgeführt.

In Zeile 15 angekommen sind alle Versprechen, die fetchIssues gegeben wurden, eingelöst. Das heißt, alle Prozesse, auf deren Abarbeitung gewartet werden musste, sind abgeschlossen. Jetzt kann  das ‚hinausgeschobene Versprechen‘, welches dem Aufrufer über deferred.promise() geben wurde, durch .resolve() gehalten werden. Der Funktion können optionale Parameter (z. B. das Ergebnis einer Berechnung) mitgegeben werden. Die Callback-Funktionen, die auf den Abschluss des Prozesses bzw. die Auflösung des Deferred warten, werden mit diesen Parametern aufgerufen.

Ein Versprechen brechen

In diesem Beispiel wurde die Fehlerbehandlung noch nicht berücksichtigt. Ein Promise-Objekt befindet sich nach der Erzeugung im Zustand pending. Wird zu einem späteren Zeitpunkt auf dem zugehörigen Deferred .resolve() aufgerufen, geht das Promise-Objekt in den Zustand resolved über. Die Callback-Funktionen, die .done() bzw. .then() übergeben wurden, werden ausgeführt.

Tritt in einem asynchronen Prozess ein Fehler auf, kann auf dem Deferred .reject() (mit optionalen Parametern) aufgerufen werden. Das Promise-Objekt geht in den Zustand rejected über und die Callback-Funktionen, die in .then()  und .fail() stehen, werden ausgeführt. Wenn ein Promise-Objekt einmal den Zustand resolved oder rejected eingenommen hat, kann es diesen nicht mehr wechseln.

Ausführlichere Erläuterungen zu Deferreds und dem Promise/A Modell gibt es im zweiteiligen Blogartikel von Chris Webb: Promise & Deferred objects in JavaScript

Teilen Sie diesen Beitrag

Das könnte dich auch interessieren …

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert