C'è davvero una differenza fondamentale tra callback e promesse?


94

Quando eseguo una programmazione asincrona a thread singolo, ci sono due tecniche principali che ho familiarità. Il più comune sta usando i callback. Ciò significa passare alla funzione che agisce in modo asincrono come funzione di callback come parametro. Al termine dell'operazione asincrona, verrà richiamato il callback.

Alcuni jQuerycodici tipici progettati in questo modo:

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

Tuttavia, questo tipo di codice può diventare disordinato e fortemente nidificato quando vogliamo effettuare chiamate asincrone aggiuntive una dopo l'altra al termine di quella precedente.

Quindi un secondo approccio sta usando Promises. Una promessa è un oggetto che rappresenta un valore che potrebbe non esistere ancora. È possibile impostare richiamate su di esso, che verranno invocate quando il valore è pronto per la lettura.

La differenza tra Promises e il tradizionale approccio di callback è che i metodi asincroni ora restituiscono in modo sincrono oggetti Promise, su cui il client imposta un callback. Ad esempio, codice simile usando Promises in AngularJS:

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

Quindi la mia domanda è: c'è davvero una vera differenza? La differenza sembra essere puramente sintattica.

C'è qualche motivo più profondo per usare una tecnica sull'altra?


8
Sì: i callback sono solo funzioni di prima classe. Le promesse sono monadi che forniscono un meccanismo componibile per concatenare le operazioni sui valori e che usano funzioni di ordine superiore con callback per fornire un'interfaccia conveniente.
amon,


5
@gnat: vista la qualità relativa delle due domande / risposte, il doppio voto dovrebbe essere il contrario di IMHO.
Bart van Ingen Schenau,

Risposte:


110

È giusto dire che le promesse sono solo zucchero sintattico. Tutto ciò che puoi fare con le promesse che puoi fare con i callback. In effetti, la maggior parte delle implementazioni promettenti offrono modi di conversione tra i due quando vuoi.

Il motivo profondo per cui le promesse sono spesso migliori è che sono più componibili , il che significa all'incirca che combinare più promesse "funziona", mentre la combinazione di più callback spesso non lo fa. Ad esempio, è banale assegnare una promessa a una variabile e collegarvi ulteriori gestori in un secondo momento, o persino collegare un gestore a un ampio gruppo di promesse che vengono eseguite solo dopo che tutte le promesse si risolvono. Sebbene sia possibile emulare queste cose con i callback, ci vuole molto più codice, è molto difficile fare correttamente e il risultato finale è di solito molto meno gestibile.

Uno dei modi più grandi (e più sottili) in cui le promesse ottengono la loro componibilità è la gestione uniforme dei valori di ritorno e le eccezioni non rilevate. Con i callback, il modo in cui viene gestita un'eccezione può dipendere interamente da quale dei molti callback nidificati l'ha lanciata e quale delle funzioni che accettano callback ha un tentativo / catch nella sua implementazione. Con le promesse, sai che un'eccezione che sfugge a una funzione di callback verrà catturata e passata al gestore degli errori fornito con .error()o .catch().

Per l'esempio che hai dato di un singolo callback rispetto a una singola promessa, è vero che non ci sono differenze significative. È quando hai un miliardo di callback contro un milione di promesse che il codice basato sulla promessa tende a sembrare molto più bello.


Ecco un tentativo di alcuni ipotetici codici scritti con promesse e poi con callback che dovrebbero essere abbastanza complessi da darti un'idea di cosa sto parlando.

Con le promesse:

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);
}

Con richiamate:

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);
                }
            });
        });
    });
}

Potrebbero esserci alcuni modi intelligenti per ridurre la duplicazione del codice nella versione di callback anche senza promesse, ma tutti quelli a cui riesco a pensare si riducono all'implementazione di qualcosa di molto promettente.


1
Un altro grande vantaggio delle promesse è che sono suscettibili di ulteriore "zuccheratura" con asincrono / wait o una coroutine che restituisce i valori promessi per le yieldpromesse. Il vantaggio qui è che hai la possibilità di mescolare in strutture di flusso di controllo native, che possono variare in quante operazioni asincrone eseguono. Aggiungerò una versione che mostra questo.
Acjay

9
La differenza fondamentale tra callback e promesse è l'inversione del controllo. Con i callback, l'API deve accettare un callback , ma con Promises, l'API deve fornire una promessa . Questa è la differenza principale e ha ampie implicazioni per la progettazione delle API.
cwharris,

@ChristopherHarris non sono sicuro che sarei d'accordo. avere un then(callback)metodo su Promise che accetta un callback (invece di un metodo su API che accetta questo callback) non deve fare nulla con IoC. Promise introduce un livello di riferimento indiretto che è utile per la composizione, il concatenamento e la gestione degli errori (in effetti la programmazione orientata alle ferrovie), ma il callback non viene ancora eseguito dal client, quindi non proprio l'assenza di IoC.
dragan.stepanovic,

1
@ dragan.stepanovic Hai ragione e ho usato una terminologia sbagliata. La differenza è il riferimento indiretto. Con un callback, devi già sapere cosa deve essere fatto con il risultato. Con una promessa, puoi decidere in seguito.
cwharris,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.