Gibt es wirklich einen grundlegenden Unterschied zwischen Rückrufen und Versprechen?

94

Bei der asynchronen Single-Thread-Programmierung gibt es zwei Haupttechniken, mit denen ich vertraut bin. Am häufigsten werden Rückrufe verwendet. Das bedeutet, dass an die Funktion übergeben wird, die asynchron eine Rückruffunktion als Parameter ausführt. Wenn der asynchrone Vorgang abgeschlossen ist, wird der Rückruf aufgerufen.

Ein typischer jQueryCode, der auf diese Weise entworfen wurde:

$.get('userDetails', {'name': 'joe'}, function(data) {
    $('#userAge').text(data.age);
});

Diese Art von Code kann jedoch unübersichtlich und stark verschachtelt werden, wenn nach Abschluss des vorherigen Codes nacheinander weitere asynchrone Aufrufe ausgeführt werden sollen.

Ein zweiter Ansatz ist die Verwendung von Versprechen. Ein Promise ist ein Objekt, das einen Wert darstellt, der möglicherweise noch nicht existiert. Sie können Rückrufe festlegen, die aufgerufen werden, wenn der Wert zum Lesen bereit ist.

Der Unterschied zwischen Promises und dem herkömmlichen Callback-Ansatz besteht darin, dass asynchrone Methoden jetzt synchron Promise-Objekte zurückgeben, auf die der Client einen Callback setzt. Zum Beispiel ähnlicher Code mit Promises in AngularJS:

$http.get('userDetails', {'name': 'joe'})
    .then(function(response) {
        $('#userAge').text(response.age);
    });

Meine Frage lautet also: Gibt es tatsächlich einen wirklichen Unterschied? Der Unterschied scheint rein syntaktisch zu sein.

Gibt es einen tieferen Grund, eine Technik über der anderen anzuwenden?

Aviv Cohn
quelle
8
Ja: Rückrufe sind nur erstklassige Funktionen. Versprechungen sind Monaden, die einen zusammensetzbaren Mechanismus zum Verketten von Operationen mit Werten bieten und Funktionen höherer Ordnung mit Rückrufen verwenden, um eine bequeme Schnittstelle bereitzustellen.
amon
5
@gnat: Angesichts der relativen Qualität der beiden Fragen / Antworten sollte das Duplikat anders herum lauten.
Bart van Ingen Schenau

Antworten:

110

Es ist fair zu sagen, dass Versprechen nur syntaktischer Zucker sind. Alles, was Sie mit Versprechungen tun können, können Sie mit Rückrufen tun. Tatsächlich bieten die meisten vielversprechenden Implementierungen Möglichkeiten zur Konvertierung zwischen den beiden, wann immer Sie möchten.

Der tiefe Grund, warum Versprechen oft besser sind, ist, dass sie komponierbarer sind , was ungefähr bedeutet, dass das Kombinieren mehrerer Versprechen "einfach funktioniert", während das Kombinieren mehrerer Rückrufe oft nicht funktioniert. Zum Beispiel ist es trivial, einer Variablen ein Versprechen zuzuweisen und später zusätzliche Handler hinzuzufügen, oder sogar einen Handler an eine große Gruppe von Versprechungen anzuhängen, die erst ausgeführt werden, nachdem alle Versprechungen aufgelöst wurden. Während Sie diese Dinge mit Rückrufen emulieren können, ist viel mehr Code erforderlich , es ist sehr schwierig, sie korrekt auszuführen , und das Endergebnis ist in der Regel weitaus weniger wartbar.

Eine der größten (und subtilsten) Möglichkeiten, wie Versprechungen komponiert werden können, ist die einheitliche Behandlung von Rückgabewerten und nicht erfassten Ausnahmen. Wie eine Ausnahme bei Rückrufen behandelt wird, hängt möglicherweise ganz davon ab, welche der vielen verschachtelten Rückrufe sie ausgelöst hat und welche der Funktionen, die Rückrufe entgegennehmen, einen Try / Catch in ihrer Implementierung aufweist. Mit Versprechungen wissen Sie , dass eine Ausnahme, die einer Rückruffunktion entgeht, abgefangen und an den Fehlerhandler übergeben wird, den Sie mit .error()oder bereitgestellt haben .catch().

Für das Beispiel, das Sie für einen einzelnen Rückruf gegenüber einem einzelnen Versprechen gegeben haben, gibt es zwar keinen signifikanten Unterschied. Es ist, wenn Sie eine Unmenge von Rückrufen im Vergleich zu einer Unmenge von Versprechungen haben, dass der Code, der auf Versprechungen basiert, tendenziell viel besser aussieht.


Hier ist ein Versuch mit einem hypothetischen Code, der mit Versprechungen und Rückrufen geschrieben wurde. Er sollte nur so komplex sein, dass Sie eine Vorstellung davon haben, wovon ich spreche.

Mit Versprechen:

createViewFilePage(fileDescriptor) {
    getCurrentUser().then(function(user) {
        return isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id);
    }).then(function(isAuthorized) {
        if(!isAuthorized) {
            throw new Error('User not authorized to view this resource.'); // gets handled by the catch() at the end
        }
        return Promise.all([
            loadUserFile(fileDescriptor.id),
            getFileDownloadCount(fileDescriptor.id),
            getCommentsOnFile(fileDescriptor.id),
        ]);
    }).then(function(fileData) {
        var fileContents = fileData[0];
        var fileDownloads = fileData[1];
        var fileComments = fileData[2];
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }).catch(showAndLogErrorMessage);
}

Mit Rückrufen:

createViewFilePage(fileDescriptor) {
    setupWidgets(fileContents, fileDownloads, fileComments) {
        fileTextAreaWidget.text = fileContents.toString();
        commentsTextAreaWidget.text = fileComments.map(function(c) { return c.toString(); }).join('\n');
        downloadCounter.value = fileDownloads;
        if(fileDownloads > 100 || fileComments.length > 10) {
            hotnessIndicator.visible = true;
        }
    }

    getCurrentUser(function(error, user) {
        if(error) { showAndLogErrorMessage(error); return; }
        isUserAuthorizedFor(user.id, VIEW_RESOURCE, fileDescriptor.id, function(error, isAuthorized) {
            if(error) { showAndLogErrorMessage(error); return; }
            if(!isAuthorized) {
                throw new Error('User not authorized to view this resource.'); // gets silently ignored, maybe?
            }

            var fileContents, fileDownloads, fileComments;
            loadUserFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileContents = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getFileDownloadCount(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileDownloads = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
            getCommentsOnFile(fileDescriptor.id, function(error, result) {
                if(error) { showAndLogErrorMessage(error); return; }
                fileComments = result;
                if(!!fileContents && !!fileDownloads && !!fileComments) {
                    setupWidgets(fileContents, fileDownloads, fileComments);
                }
            });
        });
    });
}

Es gibt vielleicht einige clevere Möglichkeiten, die Code-Duplizierung in der Callback-Version zu reduzieren, auch ohne Versprechen, aber alle, die ich mir vorstellen kann, laufen darauf hinaus, etwas sehr vielversprechendes zu implementieren.

Ixrec
quelle
1
Ein weiterer großer Vorteil von Versprechungen besteht darin, dass sie mit Async / Warten oder einer Coroutine, die die versprochenen Werte für yielded-Versprechungen zurückgibt, einer weiteren "Zuckerung" zugänglich sind . Der Vorteil hierbei ist, dass Sie die Möglichkeit haben, systemeigene Kontrollflussstrukturen zu mischen, die sich in der Anzahl der von ihnen ausgeführten asynchronen Vorgänge unterscheiden können. Ich werde eine Version hinzufügen, die dies zeigt.
Samstag,
9
Der grundlegende Unterschied zwischen Rückrufen und Versprechen ist die Umkehrung der Kontrolle. Bei Rückrufen muss Ihre API einen Rückruf akzeptieren , bei Versprechen muss Ihre API jedoch ein Versprechen bereitstellen . Dies ist der Hauptunterschied und hat weitreichende Auswirkungen auf das API-Design.
Cwharris
@ChristopherHarris nicht sicher, ob ich zustimmen würde. Eine then(callback)Promise-Methode, die einen Rückruf akzeptiert (anstelle einer API-Methode, die diesen Rückruf akzeptiert), muss nichts mit IoC zu tun haben. Promise führt eine Indirektionsebene ein, die für die Komposition, Verkettung und Fehlerbehandlung nützlich ist (in der Tat eisenbahnorientierte Programmierung), aber Callback wird immer noch nicht vom Client ausgeführt, so dass IoC nicht wirklich fehlt.
dragan.stepanovic
1
@ dragan.stepanovic Du hast Recht und ich habe die falsche Terminologie verwendet. Der Unterschied ist die Indirektion. Bei einem Rückruf müssen Sie bereits wissen, was mit dem Ergebnis zu tun ist. Mit einem Versprechen können Sie sich später entscheiden.
Cwharris