Alles über Zusagen (für in JavaScript geschriebene Windows Store-Apps)

Wenn Sie beim Schreiben von Windows Store-Apps in JavaScript asynchrone APIs verwenden, werden Sie Konstrukte verwenden, die als Zusagen („Promise“) bezeichnet werden. Das Schreiben von Zusagenketten für sequenzielle asynchrone Vorgänge wird schnell zur Routine.

Im Rahmen Ihrer Entwicklungsarbeit werden Sie es jedoch wahrscheinlich noch mit anderen Anwendungen von Zusagen zu tun haben, deren Funktion nicht klar ersichtlich ist. Ein gutes Beispiel hierfür ist das Optimieren der Elementrenderingfunktionen für ein ListView-Steuerelement, wie unter HTML ListView optimizing performance sample dargestellt. Wir werden darauf in einem späteren Beitrag genauer eingehen. Sehen Sie sich auch dieses kleine Kunststück an, das Josh Williams auf der //build 2012 in seinem Vortrag Deep Dive into WinJS vorstellte (leicht geändert):

 list.reduce(function callback (prev, item, i) {
    var result = doOperationAsync(item);
    return WinJS.Promise.join({ prev: prev, result: result}).then(function (v) {
        console.log(i + ", item: " + item+ ", " + v.result);
    });
})

Durch dieses Snippet werden Zusagen für parallele asynchrone Vorgänge zusammengefügt, deren Ergebnisse sequenziell in der mit list festgelegten Reihenfolge bereitgestellt werden. Wenn Sie die Funktionalität dieses Codes auf Anhieb verstehen, können Sie den Rest dieses Beitrags getrost überspringen. Falls nicht, gehen wir genauer auf die eigentliche Funktionsweise von Zusagen und deren Bedeutung in WinJS (der Windows-Bibliothek für JavaScript) ein, um ein besseres Verständnis dieser Muster zu entwickeln.

Was genau ist eine Zusage? Zusagenbeziehungen

Fangen wir mit einer grundlegenden Wahrheit an: Bei einer Zusage handelt es sich im Wesentlichen nur um ein Codekonstrukt bzw. eine Aufrufkonvention. Zusagen haben daher keine besondere Beziehung zu asynchronen Vorgängen, sie sind diesbezüglich nur äußerst nützlich. Eine Zusage ist lediglich ein Objekt, das einen Wert darstellt, der möglicherweise zukünftig verfügbar wird oder schon verfügbar ist. Gewissermaßen haben Zusagen also die gleiche Bedeutung wie im normalen Sprachgebrauch. Wenn ich Ihnen zusage, ein Dutzend Donuts zu bringen, muss ich diese Donuts in diesem Moment nicht unbedingt besitzen, nehme jedoch mit Sicherheit an, dass ich sie erhalten werde. Sobald ich die Donuts habe, liefere ich.

Eine Zusage impliziert also eine Beziehung zwischen zwei Parteien: dem Urheber, der die Zusage für eine Warenlieferung tätigt, und dem Verbraucher, der sowohl die Zusage als auch die eigentlichen Waren erhält. Die Beschaffung der Waren fällt in die alleinige Verantwortung des Urhebers, und auch der Verbraucher kann mit der Zusage und den gelieferten Waren beliebig verfahren. Er kann die Zusage sogar mit anderen Verbrauchern teilen.

Die Beziehung zwischen Urheber und Verbraucher weist außerdem zwei Phasen auf, Erstellung und Erfüllung. Sämtliche dieser Aspekte werden in folgendem Diagramm dargestellt.

Diagramm – Zusagen

Wenn wir dem Diagrammfluss folgen, wird deutlich, weshalb Zusagen aufgrund der beiden Beziehungsphasen gut für eine asynchrone Bereitstellung geeignet sind. Das Wichtigste ist, dass der Verbraucher nach Erhalt der Anforderungsbestätigung (der Zusage) anderen Aufgaben nachgehen kann (asynchron), anstelle zu warten (synchron). Der Verbraucher kann so die Zeit, während der er auf die Erfüllung der Zusage wartet, für Anderes nutzen, z. B. zum Beantworten weiterer Anforderungen. Dies ist der eigentliche Zweck asynchroner APIs. Falls die Waren bereits verfügbar sind, kann die Zusage unmittelbar erfüllt werden, wodurch der gesamte Vorgang zu einer Art synchroner Aufrufkonvention wird.

Natürlich müssen im Zusammenhang mit dieser Beziehung noch einige weitere Aspekte berücksichtigt werden. Sie haben sicherlich im wirklichen Leben schon Zusagen getätigt und erhalten. Sicher wurden viele dieser Zusagen erfüllt, einige jedoch auch nicht eingehalten: Der Pizzalieferant kann auf dem Weg zu Ihnen durchaus in einen Unfall verwickelt werden! Nicht eingehaltene Zusagen sind unumgänglich und können weder im echten Leben noch bei der asynchronen Programmierung vermieden werden.

In Bezug auf die Zusagenbeziehung benötigt der Urheber daher eine Möglichkeit zur Mitteilung, dass er die Zusage nicht einhalten kann, und der Verbraucher muss darüber informiert werden können. Zudem werden Verbraucher möglicherweise auch ungeduldig, wenn Zusagen längere Zeit nicht erfüllt werden. Wenn der Urheber also den Fortschritt bis zur Erfüllung der Zusage nachverfolgen kann, ist auch eine entsprechende Methode zum Informieren des Verbrauchers erforderlich. Drittens ist es außerdem möglich, dass der Verbraucher den Auftrag abbricht und dem Urheber mitteilt, dass die Waren nicht mehr benötigt werden.

Wenn diese Anforderungen im Diagramm berücksichtigt werden, ergibt sich ein Bild der vollständigen Beziehung:

Diagramm – Zusagen 2

Betrachten wir nun, wie diese Beziehungen in Code umgesetzt werden.

Das Zusagenkonstrukt und Zusagenketten

Tatsächlich gibt es für Zusagen eine Reihe verschiedener Vorschläge oder Spezifikationen. In Windows und WinJS werden als Common JS/Promises A bezeichnete Zusagen verwendet, wobei eine Zusage (der Rückgabewert eines Urhebers, der einen zukünftig bereitzustellenden Wert repräsentiert) ein Objekt mit einer then genannten Funktion darstellt. Die Verbraucher abonnieren die Einhaltung der Zusage durch das Aufrufen von then. (Für Zusagen in Windows wird auch die ähnliche Funktion done unterstützt, die wie in Kürze erläutert in Zusagenketten verwendet wird.)

An diese Funktion werden vom Verbraucher in folgender Reihenfolge bis zu drei optionale Funktionen als Argumente übergeben:

  1. Ein Completed-Handler. Diese Funktion wird vom Urheber aufgerufen, sobald der zugesagte Wert zur Verfügung steht. Ist dieser Wert bereits verfügbar, wird der Completed-Handler unmittelbar (synchron) innerhalb von then aufgerufen.
  2. Ein optionaler Fehlerhandler, der aufgerufen wird, falls der zugesagte Wert nicht beschafft werden kann. Nach einem Aufruf des Fehlerhandlers wird für die entsprechende Zusage nie der Completed-Handler aufgerufen.
  3. Ein optionaler Progress-Handler, der bei entsprechender Unterstützung durch den Vorgang regelmäßig für Zwischenergebnisse aufgerufen wird. (In WinRT bedeutet dies, dass die API den Rückgabewert IAsync[Action | Operation]WithProgress aufweist, im Gegensatz zu jenen mit IAsync[Action | Operation] .)

Beachten Sie, dass für jedes dieser Argumente null übergeben werden kann, beispielsweise wenn Sie statt eines Completed-Handlers nur einen Fehlerhandler hinzufügen möchten.

Auf der anderen Seite der Beziehung können Verbraucher beliebig viele Handler für dieselbe Zusage abonnieren, indem sie mehrfach then aufrufen. Die Zusage kann auch mit anderen Verbrauchern geteilt werden, die ebenso häufig wie gewünscht then aufrufen können. Dies wird vollständig unterstützt.

Für Zusagen bedeutet dies, dass Listen aller empfangener Handler verwaltet werden müssen, um diese zum jeweils richtigen Zeitpunkt aufzurufen. Wie in der vollständigen Beziehung dargestellt, ist bei Zusagen außerdem eine Möglichkeit zum Abbrechen erforderlich.

Aus der Promises A-Spezifikation ergibt sich die zusätzliche Anforderung, dass die then-Methode selbst eine Zusage zurückgeben muss. Diese zweite Zusage gilt als erfüllt, sobald der an die erste promise.then-Methode übergebene Completed-Handler zurückgegeben wird. Der Rückgabewert wird hierbei als das Ergebnis der zweiten Zusage bereitgestellt. Gehen Sie diesen Codeausschnitt durch:

 var promise1 = someOperationAsync();
var promise2 = promise1.then(function completedHandler1 (result1) { return 7103; } );
promise2.then(function completedHandler2 (result2) { });

Die Ausführungskette hierbei verläuft folgendermaßen: Zunächst wird someOperationAsync gestartet und gibt promise1 zurück. Während dieses Vorgangs wird die promise1.then-Methode aufgerufen, die unmittelbar promise2 zurückgibt. Berücksichtigen Sie stets, dass completedHandler1 erst aufgerufen wird, wenn das Ergebnis des asynchronen Vorgangs bereits verfügbar ist. Nehmen wir an, dass noch auf das Ergebnis gewartet wird. Daher gehen wir direkt zum Aufruf von promise2.then über, wobei zu diesem Zeitpunkt completedHandler2 auch hier nicht aufgerufen wird.

Etwas später wird someOperationAsync mit einem Wert von beispielsweise 14618 abgeschlossen. Da promise1 nun erfüllt ist, wird mit diesem Wert completedHandler1 aufgerufen, sodass result1 14618 beträgt. Jetzt wird completedHandler1 ausgeführt und gibt den Wert 7103 zurück. Zu diesem Zeitpunkt ist promise2 erfüllt, sodass completedHandler2 mit dem Wert result2 von 7103 aufgerufen wird.

Was jedoch geschieht, wenn ein Completed-Handler eine weitere Zusage zurückgibt? Dieser Fall wird etwas anders verarbeitet. Angenommen, der im obigen Code aufgeführte completedHandler1 gibt eine Zusage zurück, z. B. so:

 var promise2 = promise1.then(function completedHandler1 (result1) {
    var promise2a = anotherOperationAsync();
    return promise2a;
});

In diesem Fall ist result2 in completedHandler2 nicht promise2a selbst, sondern der fulfillment-Wert von promise2a. Da der Completed-Handler eine Zusage zurückgegeben hat, hat dies zur Folge, dass promise2, wie von promise1.then zurückgegeben, mit den Ergebnissen aus promise2a erfüllt wird.

Genau dieses Merkmal ermöglicht die Verkettung sequenzieller asynchroner Vorgänge, wobei die Ergebnisse jedes Vorgangs in der Kette vom nachfolgenden Vorgang genutzt werden. Ohne Zwischenvariablen oder benannte Handler weisen Zusagenketten häufig dieses Muster auf:

 operation1().then(function (result1) {
    return operation2(result1)
}).then(function (result2) {
    return operation3(result2);
}).then(function (result3) {
    return operation4(result3);
}).then(function (result4) {
    return operation5(result4)
}).then(function (result5) {
    //And so on
});

Wahrscheinlich werden die empfangenen Ergebnisse von den jeweiligen Completed-Handler noch weiter verarbeitet, alle Ketten verfügen jedoch über diese Kernstruktur. Ebenso trifft zu, dass hier sämtliche then-Methoden direkt nacheinander ausgeführt werden, da sie lediglich den entsprechenden Completed-Handler speichern und eine weitere Zusage zurückgeben. Am Codeende wurde also lediglich operation1 gestartet. Completed-Handler wurden nicht aufgerufen. Im Rahmen der zahlreichen then-Aufrufe wurde jedoch eine Reihe von Zwischenzusagen erstellt und untereinander verknüpft, um die Kette beim Fortsetzen der sequenziellen Vorgänge zu verwalten.

Bemerkenswerterweise kann die gleiche Sequenz durch das Schachteln aller nachfolgenden Vorgänge im vorherigen Completed-Handler erzielt werden, wobei die gesamten return-Anweisungen entfallen. Verschachtelungen dieser Art führen jedoch zu sehr komplizierten Einzugsszenarien, insbesondere, wenn Sie bei jedem then-Aufruf Progress- und Fehlerhandler hinzufügen.

Eine Funktion von Zusagen in WinJS ist außerdem, dass Fehler in einem beliebigen Kettenabschnitt automatisch an das Kettenende verteilt werden. Sie müssen also dem letzten then-Aufruf einfach nur einen einzigen Fehlerhandler hinzufügen, ohne Handler auf jeder Ebene zu verwenden. Der Haken ist jedoch, dass diese Fehler aus verschiedenen komplexen Gründen verloren gehen, wenn es sich beim letzten Kettenabschnitt um einen then-Aufruf handelt. Deshalb stellt WinJS für Zusagen außerdem eine done-Methode bereit. Diese Methode akzeptiert dieselben Argumente wie then, gibt jedoch an, dass die Kette abgeschlossen ist. (Anstelle einer weiteren Zusage wird undefined zurückgegeben.) Falls done ein Fehlerhandler hinzugefügt wurde, wird dieser für alle Fehler in der gesamten Kette aufgerufen. Weiterhin löst done eine Ausnahme auf App-Ebene aus, wenn kein Fehlerhandler verfügbar ist, die durch window.onerror von WinJS.Application.onerror-Ereignissen behandelt werden kann. Kurz gesagt: Im Idealfall sollten alle Ketten mit done enden, um sicherzustellen, dass Ausnahmen zum Vorschein kommen und ordnungsgemäß behandelt werden.

Wenn Sie natürlich eine Funktion schreiben, deren Zweck darin besteht, die letzte Zusage aus einer langen Kette von then-Aufrufen zurückzugeben, verwenden Sie am Ende weiterhin then. Für die Fehlerbehandlung ist dann die aufrufende Funktion zuständig, von der diese Zusage möglicherweise in einer vollkommen anderen Kette verwendet wird.

Erstellen von Zusagen: die WinJS.Promise-Klasse

Sie können zwar jederzeit auf Grundlage der Promises A-Spezifikation eigene Promise-Klassen erstellen, tatsächlich ist dies jedoch mit beträchtlichem Arbeitsaufwand verbunden, weshalb sich eine Bibliothek hierfür besser eignet. WinJS stellt daher die stabile, ausführlich getestete und flexible Promise-Klasse WinJS.Promise bereit. So können Sie mühelos Zusagen für verschiedene Werte und Vorgänge erstellen, ohne die Feinheiten der Urheber/Verbraucher-Beziehungen oder das Verhalten der then-Methode verwalten zu müssen.

Bei Bedarf können (und sollten) Sie die neue WinJS.Promise-Klasse (oder eine geeignete Hilfsfunktion, wie im nächsten Abschnitt erläutert) verwenden, um Zusagen sowohl für asynchrone Vorgänge als auch für bestehende (synchrone) Werte zu erstellen. Denken Sie daran, dass eine Zusage lediglich ein Codekonstrukt ist: Zusagen müssen weder einen asynchronen Vorgang noch beliebige andere asynchrone Elemente umschließen. Ebenso führt das einfache Umschließen eines Codeabschnitts mit einer Zusage nicht automatisch zur asynchronen Ausführung . Für dieses Verhalten müssen Sie weiterhin selbst sorgen.

Als einfaches Beispiel für eine direkte Verwendung von WinJS.Promise nehmen wir an, dass eine lange Berechnung ausgeführt werden soll (eine einfache Addition von Zahlen, beginnend bei 1 bis zu einem Maximalwert), jedoch als asynchroner Vorgang. Wir könnten einen eigenen Callbackmechanismus für eine derartige Routine entwickeln, wenn wir jedoch eine Zusage als Umschließung einsetzen, kann sie mit Zusagen anderer APIs verkettet oder verknüpft werden. (Auf ähnliche Weise umschließt die WinJS.xhr-Funktion das asynchrone XmlHttpRequest-JavaScript-Objekt mit einer Zusage, damit Sie sich nicht mit der besonderen Ereignisstruktur des Objekts befassen müssen.)

Für lange Berechnungen kann natürlich auch ein JavaScript-Worker verwendet werden, zur Veranschaulichung bleiben wir jedoch im UI-Thread und nutzen setImmediate, um den Vorgang in Schritte zu unterteilen. Gehen Sie folgendermaßen vor, um eine Zusagenstruktur mit WinJS.Promise zu implementieren:

 function calculateIntegerSum(max, step) {
    //The WinJS.Promise constructor's argument is an initializer function that receives 
    //dispatchers for completed, error, and progress cases.
    return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
        var sum = 0;

        function iterate(args) {
            for (var i = args.start; i < args.end; i++) {
                sum += i;
            };

            if (i >= max) {
                //Complete--dispatch results to completed handlers
                completeDispatch(sum);
            } else {
                //Dispatch intermediate results to progress handlers
                progressDispatch(sum);
                setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) });
            }
        }
            
        setImmediate(iterate, { start: 0, end: Math.min(step, max) });
    });
}

Beim Aufrufen der neuen WinJS.Promise-Klasse erhält deren Konstruktor als einziges Argument eine Initialisiererfunktion (in diesem Fall anonym). Der Initialisierer kapselt den auszuführenden Vorgang. Beachten Sie jedoch stets, dass sich diese Funktion im UI-Thread synchron selbst ausführt. Falls wir also an dieser Stelle eine lange Berechnung ausführen, ohne setImmediate zu verwenden, wäre der UI-Thread während des ganzen Zeitraums blockiert. Auch in diesem Fall wird der Code durch das Umschließen mit einer Zusage nichtautomatisch asynchron ausgeführt. Dies muss mithilfe der Initialisierungsfunktion festgelegt werden.

Als Argumente empfängt die Initialisiererfunktion drei Verteiler für die von Zusagen unterstützten Fälle Completed, Fehler und Progress. Wie Sie sehen, werden diese Verteiler während des Vorgangs zu geeigneten Zeitpunkten mit den richtigen Argumenten aufgerufen.

Ich bezeichne diese Funktionen als „Verteiler“, da sie sich von den Handlern unterscheiden, mit denen Verbraucher die then-Methode (bzw. done) der Zusage abonnieren. Hinter den Kulissen werden von WinJS Arrays dieser Handler verwaltet, weshalb eine beliebige Anzahl von Verbrauchern so viele Handler wie gewünscht abonnieren kann. Wenn Sie einen der Verteiler aufrufen, durchläuft WinJS die interne Liste und ruft sämtliche entsprechenden Handler für Sie auf. WinJS.Promise stellt außerdem sicher, dass von der then-Methode eine zum Verketten erforderliche weitere Zusage zurückgegeben wird.

Kurz gesagt: WinJS.Promise stellt alle erforderlichen Elemente im Zusammenhang mit Zusagen bereit. So können Sie sich auf den durch die Zusage dargestellten, von der Initialisiererfunktion gekapselten Kernvorgang konzentrieren.

Hilfsfunktionen zum Erstellen von Zusagen

Die statische WinJS.Promise.as-Methode stellt die primäre Hilfsfunktion zum Erstellen einer Zusage dar, da mit dieser Methode ein beliebiger Wert von einer Zusage umschlossen werden kann. Ein solcher Wrapper um einen bereits vorhandenen Wert wird einfach direkt in umgekehrter Richtung ausgeführt und ruft alle an then übergebenen Completed-Handler auf. Hierdurch können insbesondere beliebige bekannte Werte als Zusagen behandelt werden, um sie mit anderen Zusagen zu mischen oder zusammenzufassen (durch Verknüpfen und Verketten). Wenn Sie as auf eine bestehende Zusage anwenden, wird lediglich diese Zusage zurückgegeben.

Bei der anderen statischen Hilfsfunktion handelt es sich um WinJS.Promise.timeout, einen praktischen Wrapper für setTimeout und setImmediate. Außerdem können Sie eine Zusage erstellen, die eine zweite Zusage abbricht, falls letztere nicht innerhalb einer bestimmten Anzahl von Millisekunden erfüllt wird.

Berücksichtigen Sie, dass die timeout-Zusagen um setTimeout und setImmediate ebenfalls durch undefined erfüllt werden. Es stellt sich daher die Frage, wie nach dem Timeout von diesen Zusagen ein anderer Wert bereitgestellt werden kann. Dies ist aufgrund der Tatsache möglich, dass then eine weitere Zusage zurückgibt, die durch den Rückgabewert des Completed-Handlers erfüllt wird. Betrachten Sie beispielsweise diese Codezeile:

 var p = WinJS.Promise.timeout(1000).then(function () { return 12345; });

Dieser Code erstellt die Zusage p, die nach einer Sekunde durch den Wert 12345 erfüllt wird. Anders ausgedrückt: WinJS.Promise.timeout(…).then(function () { return <value>} ) stellt ein Muster dar, um nach dem angegebenen Timeout <value> bereitzustellen. Wenn es sich zudem bei <value> um eine weitere Zusage handelt, dient das Muster zum Bereitstellen des Erfüllungswerts aus dieser Zusage an einem Zeitpunkt nach dem Timeout.

Abbrechen von Zusagen und Fehlergenerierung

Vielleicht sind Ihnen die beiden Schwachstellen im oben dargestellten Code aufgefallen. Erstens besteht keine Möglichkeit, den Vorgang abzubrechen, sobald er gestartet wurde. Der zweite Mangel ist die unzureichende Fehlerbehandlung.

In beiden Fällen liegt die Lösung darin, dass zusagengenerierende Funktionen wie calculateIntegerSum immer eine Zusage zurückgeben müssen. Wenn ein Vorgang nicht abgeschlossen oder nicht einmal gestartet werden kann, weist diese Zusage den sogenannten Fehlerzustand auf. Dies bedeutet, dass die Zusage weder jetzt noch irgendwann über ein Ergebnis verfügt, das an Completed-Handler übergeben werden kann – es werden stets ausschließlich die Fehlerhandler der Zusage aufgerufen. Tatsächlich wird, falls ein Verbraucher then für eine sich bereits im Fehlerzustand befindende Zusage aufruft, von der Zusage unmittelbar (synchron) der an then übergebene Fehlerhandler aufgerufen.

Ein WinJS.Promise-Objekt wechselt aus zwei Gründen in den Fehlerzustand: Entweder ruft der Verbraucher die cancel-Methode des Objekts auf, oder der Fehlerverteiler wird durch den Code innerhalb der Initialisierfunktion aufgerufen. In diesem Fall wird jeglicher Fehlerwert, der in der Zusage aufgefangen oder propagiert wurde, an die Fehlerhandler übergeben. Beim Erstellen eines Vorgangs innerhalb eines WinJS.Promise-Objekts können Sie auch eine Instanz von WinJS.ErrorFromName verwenden. Hierbei handelt es sich einfach um ein JavaScript-Objekt mit einer name-Eigenschaft, die den Fehler identifiziert, und einer message-Eigenschaft für weitere Informationen. Beispielsweise empfangen Fehlerhandler beim Abbrechen einer Zusage ein Fehlerobjekt, in dem sowohl name als auch message auf „Abgebrochen“ festgelegt sind.

Welche Möglichkeiten bestehen jedoch, wenn der Vorgang überhaupt nicht gestartet werden kann? Wenn Sie beispielsweise calculateIntegerSum mit falschen Argumenten (wie 0, 0) aufrufen, sollte die Funktion direkt eine Zusage im Fehlerzustand zurückgeben, anstatt mit der Zählung zu beginnen. Dies ist der Zweck der statischen WinJS.Promise.wrapError-Methode. Diese Methode akzeptiert eine WinJS.ErrorFromName-Instanz und gibt eine Zusage zurück, die in diesem Fall anstelle einer neuen WinJS.Promise-Instanz zurückgegeben würde.

Der zweite Aspekt, der in diesem Zusammenhang berücksichtigt werden muss, ist folgender: Zwar versetzt das Aufrufen der cancel-Methode einer Zusage diese in den Fehlerzustand, wie jedoch kann der asynchrone Vorgang beendet werden, der gerade ausgeführt wird? In der vorherigen Implementierung ruft die calculateIntegerSum-Funktion einfach solange setImmediate, bis der Vorgang abgeschlossen ist, unabhängig vom Zustand der erstellten Zusage. Tatsächlich wird, falls der Vorgang nach dem Abbrechen der Zusage den Abschlussverteiler aufruft, dieser Abschluss von der Zusage einfach ignoriert.

Die Zusage benötigt daher eine Möglichkeit zur Benachrichtigung des Vorgangs, dass die Verarbeitung beendet werden kann. Hierfür akzeptiert der WinJS.Promise-Konstruktor ein zweites Funktionsargument, das beim Abbruch der Zusage aufgerufen wird. In unserem Beispiel müsste das Aufrufen dieser Funktion den nächsten Aufruf von setImmediate verhindern und dadurch die Berechnung stoppen. Zusammen mit ordnungsgemäßer Fehlerbehandlung ergibt sich so der folgende Code:

 function calculateIntegerSum(max, step) {
    //Return a promise in the error state for bad arguments
    if (max < 1 || step < 1) {
        var err = new WinJS.ErrorFromName("calculateIntegerSum", "max and step must be 1 or greater");
        return WinJS.Promise.wrapError(err);
    }

    var _cancel = false;

    //The WinJS.Promise constructor's argument is an initializer function that receives 
    //dispatchers for completed, error, and progress cases.
    return new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
        var sum = 0;

        function iterate(args) {
            for (var i = args.start; i < args.end; i++) {
                sum += i;
            };

            //If for some reason there was an error, create the error with WinJS.ErrorFromName
            //and pass to errorDispatch
            if (false /* replace with any necessary error check -- we don’t have any here */) {
                errorDispatch(new WinJS.ErrorFromName("calculateIntegerSum (scenario 7)", "error occurred"));
            }

            if (i >= max) {
                //Complete--dispatch results to completed handlers
                completeDispatch(sum); 
            } else {
                //Dispatch intermediate results to progress handlers
                progressDispatch(sum);

                //Interrupt the operation if canceled
                if (!_cancel) {
                    setImmediate(iterate, { start: args.end, end: Math.min(args.end + step, max) });
                }
            }
        }
            
        setImmediate(iterate, { start: 0, end: Math.min(step, max) });
    },
    //Cancellation function for the WinJS.Promise constructor
    function () {
        _cancel = true;
    });
}

Insgesamt gesehen bietet das Erstellen von WinJS.Promise-Instanzen vielfältige Anwendungsmöglichkeiten. Wenn z. B. eine Bibliothek über eine andere asychrone Methode mit einem Webdienst kommuniziert, können Sie diese Vorgänge mit Zusagen umschließen. Sie können auch mehrere asynchrone Vorgänge (oder andere Zusagen!) aus verschiedenen Quellen in einer neuen Zusage kombinieren, für den Fall, dass die Steuerung aller beteiligten Beziehungen erforderlich ist. Selbstverständlich können Sie eigene Handler für andere asynchrone Vorgänge und deren Zusagen in den Code eines Initialisierers des WinJS.Promise-Objekts einfügen. Sie können diese zum Kapseln automatischer Wiederholungsmechanismen bei Netzwerktimeouts o. Ä. verwenden, als Hook in eine generische Fortschrittsanzeige-Benutzeroberfläche einfügen oder für versteckte Protokollierung oder Analysen einsetzen. Bei jeder dieser Anwendungen benötigt der restlichen Code keine Informationen zu den Details und kann einfach nur Zusagen von der Verbraucherseite verarbeiten.

Auf ähnliche Weise kann ein JavaScript-Worker ohne großen Aufwand von einer Zusage umschlossen werden, wodurch dessen Darstellung und Verhalten anderen asynchronen WinRT-Vorgängen entspricht. Bekanntermaßen stellen Worker ihre Ergebnisse durch einen postMessage-Aufruf bereit, der im Worker-Objekt in der App ein message-Ereignis auslöst. Der folgende Code verknüpft dieses Ereignis anschließend mit einer Zusage, die unabhängig von den Ergebnissen der jeweiligen Meldung erfüllt ist:

 // This is the function variable we're wiring up.
var workerCompleteDispatch = null;

var promiseJS = new WinJS.Promise(function (completeDispatch, errorDispatch, progressDispatch) {
    workerCompleteDispatch = completeDispatch;
});

// Worker is created here and stored in the 'worker' variable

// Listen for worker events
worker.onmessage = function (e) {
    if (workerCompleteDispatch != null) {
        workerCompleteDispatch(e.data.results); /* event args depends on the worker */
    }
}

promiseJS.done(function (result) {
    // Output for JS worker
});

Zum Erweitern dieses Codes zur Behandlung von Workerfehlern können Sie den Fehlerverteiler in einer anderen Variablen speichern, die Ereignisargumente des message-Ereignishandlers auf Fehlerinformationen überprüfen lassen und dann je nach Ergebnis den Fehlerverteiler anstelle des Abschlussverteilers aufrufen.

Verknüpfen paralleler Zusagen

Da Zusagen häufig zum Umschließen asynchroner Vorgänge verwendet werden, ist es durchaus möglich, dass mehrere Vorgänge parallel ausgeführt werden. In diesen Fällen muss ggf. ermittelt werden können, wann eine Zusage bzw. alle Zusagen in einer Gruppe erfüllt sind. Diesem Zweck dienen die statischen Funktionen WinJS.Promise.any und WinJS.Promise.join.

Beide Funktionen akzeptieren ein Wertearray oder ein Objekt mit Werteigenschaften. Bei diesen Werten kann es sich um Zusagen und beliebige andere, von WinJS.Promise.as umschlossene Werte handeln, sodass das ganze Array oder Objekt aus Zusagen besteht.

Merkmale von any:

  • Durch any wird eine einzelne Zusage erstellt, die bei der Erfüllung oder beim Auftreten eines Fehlers in einer der anderen Zusagen erfüllt wird (ein logisches OR). Im Grunde fügt anyall diesen Zusagen Completed-Handler hinzu. Sobald dann ein Completed-Handler aufgerufen wird, ruft dieser alle von der any-Zusage selbst empfangenen Completed-Handler auf.
  • Nach Erfüllung der any-Zusage (d. h. bei Erfüllung der ersten Zusage auf der Liste) werden die anderen Vorgänge auf der Liste weiter ausgeführt und rufen jegliche Completed-, Fehler- oder Progress-Handler auf, die diesen Zusagen individuell zugeordnet sind.
  • Wenn Sie die any-Zusage abbrechen, werden auch alle Zusagen auf der Liste abgebrochen.

Merkmale von join:

  • Durch join wird eine einzelne Zusage erstellt, die bei der Erfüllung oder beim Auftreten eines Fehlers in allen anderen Zusagen erfüllt wird (ein logisches AND). Im Grunde fügt joinall diesen Zusagen Completed- sowie Fehlerhandler hinzu und wartet dann auf den Aufruf sämtlicher dieser Handler. Erst dann ruft join alle selbst empfangenen Completed-Handler auf.
  • Die join-Zusage meldet auch den Fortschritt an alle angegebenen Progress-Handler. In diesem Fall handelt es sich beim Zwischenergebnis um ein Array der Ergebnisse aller bislang erfüllten Zusagen.
  • Wenn Sie die join-Zusage abbrechen, werden auch alle noch ausstehenden Zusagen abgebrochen.

Neben any und join sollen an dieser Stelle noch zwei weitere praktische, statische WinJS.Promise-Methoden erwähnt werden:

  • is ermittelt, ob es sich bei einem beliebigen Wert um eine Zusage handelt, und gibt einen booleschen Wert zurück. Im Grunde stellt die Methode sicher, dass es sich um ein Objekt mit einer als „then“ bezeichneten Funktion handelt. Tests in Bezug auf „done“ erfolgen nicht.
  • theneachwendet Completed-, Fehler- und Progress-Handler auf eine Gruppe von Zusagen an (mithilfe von then) und gibt die Ergebnisse als eine weitere Wertegruppe innerhalb einer Zusage zurück. Jeder der Handler kann NULL sein.

Parallele Zusagen mit sequenziellen Ergebnissen

Durch WinJS.Promise.join und WinJS.Promise.any können parallele Zusagen verwendet werden, also parallele asynchrone Vorgänge. Die von join zurückgegebene Zusage wird wie bereits erwähnt erfüllt, wenn alle Zusagen in einem Array erfüllt sind. Diese Zusagen werden jedoch wahrscheinlich in zufälliger Reihenfolge abgeschlossen. Was wäre, wenn Sie über eine Reihe von auf diese Weise ausführbaren Vorgängen verfügen, deren Ergebnisse jedoch in wohldefinierter Reihenfolge verarbeitet werden sollen, und zwar der Reihenfolge der jeweiligen Zusagen in einem Array?

Hierfür muss jede nachfolgende Zusage mit der join-Methode aller vorherigen Zusagen verknüpft werden. Der Codeausschnitt am Anfang dieses Beitrags bewirkt genau dies. Hier ist der gleiche Code, jedoch für explizite Zusagen umgeschrieben. (Stellen Sie sich vor, listsei ein Array beliebiger Werte, die als Argumente für einen hypothetischen, zusagengenerierenden asynchronen doOperationAsync-Aufruf dienen):

 list.reduce(function callback (prev, item, i) {
    var opPromise = doOperationAsync(item);
    var join = WinJS.Promise.join({ prev: prev, result: opPromise});

    return join.then(function completed (v) {
        console.log(i + ", item: " + item+ ", " + v.result);
    });
})

Um ein Verständnis dieses Codes zu entwickeln, sollten wir uns zunächst mit der Funktionsweise der reduce-Methode des Arrays vertraut machen. reduce ruft für jedes Element im Array das hier als callback bezeichnete Funktionsargument auf, das vier Argumente empfängt (wovon im Code nur drei verwendet werden):

  • prev – Der Wert, der vom vorherigen Aufruf von callback zurückgegeben wurde (NULL für das erste Element).
  • item – Der aktuelle Wert aus dem Array.
  • i – Der Index des Elements in der Liste.
  • source – Das ursprüngliche Array.

Für das erste Element auf der Liste erhalten wir eine opPromise1 genannte Zusage. Da prev den Wert NULL aufweist, verknüpfen wir [WinJS.Promise.as(null), opPromise1] . Beachten Sie, das nicht join selbst zurückgegeben wird. Stattdessen fügen wir dieser Verknüpfung einen Completed-Handler (mit der Bezeichnung completed) hinzu und geben die Zusage von then zurück.

Denken Sie daran, dass die von then zurückgegebene Zusage bei Rückgabe des Completed-Handlers erfüllt wird. Deshalb gibt callback eine Zusage zurück, die erst erfüllt wird, wenn der completed-Handler die Ergebnisse von opPromise1 verarbeitet hat. Wenn Sie zudem an das Ergebnis einer Verknüpfung zurückdenken, wird diese durch ein Objekt mit den Ergebnissen der Zusagen auf der ursprünglichen Liste erfüllt. Dies bedeutet, dass der Erfüllungswert v sowohl eine prev-Eigenschaft als auch eine result-Eigenschaft aufweist, wobei letztere das Ergebnis von opPromise1 ist.

Beim nächsten list-Element wird von callback ein prev-Wert mit dem vorherigen join.then-Ergebnis empfangen. Anschließend erstellen wir eine neue Verknüpfung zwischen opPromise1.then und opPromise2. Das Ergebnis: Diese Verknüpfung wird erst abgeschlossen, wenn sowohl opPromise2 erfüllt ist und der Completed-Handler für opPromise1 zurückgegeben wird. Voila! Der completed2-Handler, den wir nun dieser join-Methode hinzufügen, wird erst nach Rückgabe von completed1 aufgerufen.

Die gleichen Abhängigkeiten bauen sich für jedes Listenelement weiter auf: Die von join.then für Element n zurückgegebene Zusage wird erst bei Rückgabe von completedn** erfüllt. Dies stellt sicher, dass die Completed-Handler in der gleichen Reihenfolge wie list aufgerufen werden.

Zusammenfassung

In diesem Beitrag haben wir erläutert, dass eine Zusage lediglich ein (wenn auch leistungsstarkes) Codekonstrukt oder eine Aufrufkonvention ist. Sie stellt die Beziehung zwischen einem Urheber dar, der zu einem beliebigen späteren Zeitpunkt Werte liefern muss, und einem Verbraucher, der über den Verfügbarkeitszeitpunkt dieser Werte informiert werden möchte. Hierbei sind Zusagen sehr gut zum Darstellen von Ergebnissen asynchroner Vorgänge geeignet und werden umfassend für in JavaScript geschriebene Windows Store-Apps verwendet. Die Spezifikation für Zusagen ermöglicht außerdem das sequenzielle Verketten asynchroner Vorgänge, in denen das Zwischenergebnis jedes Kettenglieds an das nächste übermittelt wird.

Die Windows-Bibliothek für JavaScript WinJS ermöglicht das stabile Implementieren von Zusagen, mit denen Sie beliebige Arten eigener Vorgänge umschließen können. Außerdem bietet sie Hilfsfunktionen für allgemeine Szenarien wie das Verknüpfen von Zusagen für parallele Vorgänge. Dank WinJS können auf diese Weise asynchrone Vorgänge sehr effizient und effektiv genutzt werden.

Kraig Brockschmidt

Programmmanager, Windows Ecosystem Team

Autor, Programming Windows 8 Apps in HTML, CSS, and JavaScript