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

92

Quando si esegue la programmazione asincrona a thread singolo, ci sono due tecniche principali che sono familiari. Il più comune utilizza 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 tipici codici jQuery progettati in questo modo:

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

Tuttavia questo tipo di codice può diventare disordinato e altamente annidato quando vogliamo effettuare chiamate asincrone una dopo l'altra quando finisce quella precedente.

Quindi un secondo approccio sta usando Promises. Una promessa è un oggetto che rappresenta un valore che potrebbe non esistere ancora. È possibile impostare callback su di esso, che verranno richiamati quando il valore è pronto per essere letto.

La differenza tra Promises e il tradizionale approccio callbacks è che i metodi asincroni ora restituiscono in modo sincrono gli oggetti Promise, su cui il client imposta una 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'è una ragione più profonda per usare una tecnica sull'altra?

    
posta Aviv Cohn 12.11.2015 - 23:26
fonte

1 risposta

109

È 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 forniscono modi di convertire tra i due ogni volta che vuoi.

La ragione profonda per cui le promesse sono spesso migliori è che sono più componibili , il che significa che combinare più promesse "funziona", mentre la combinazione di più callback spesso non lo è. Ad esempio, è banale assegnare una promessa a una variabile e allegare ulteriori gestori ad esso in un secondo momento, o persino associare un gestore a un grande gruppo di promesse che viene eseguito solo dopo che tutte le promesse si risolvono. Sebbene sia possibile emulare queste cose con i callback, richiede molto più codice, è molto difficile da eseguire correttamente e il risultato finale è solitamente molto meno gestibile.

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

Per l'esempio che hai fornito di un singolo callback rispetto a una singola promessa, è vero che non c'è alcuna differenza significativa. È quando hai una zillion di callback contro una zillion promette che il codice basato sulla promessa tende ad apparire molto più bello.

Ecco un tentativo di codice ipotetico scritto con promesse e poi con callback che dovrebbero essere abbastanza complessi da darti un'idea di cosa sto parlando.

Con 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 callback:

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 callbacks anche senza promesse, ma tutti quelli che posso pensare si riducono ad implementare qualcosa di molto promettente.

    
risposta data 12.11.2015 - 23:37
fonte