¿Existe realmente una diferencia fundamental entre devoluciones de llamada y promesas?

94

Al hacer una programación asincrónica de un solo subproceso, hay dos técnicas principales con las que estoy familiarizado. El más común es usar devoluciones de llamada. Eso significa pasar a la función que actúa asincrónicamente una función de devolución de llamada como parámetro. Cuando finalice la operación asincrónica, se llamará a la devolución de llamada.

Algún jQuerycódigo típico diseñado de esta manera:

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

Sin embargo, este tipo de código puede volverse desordenado y altamente anidado cuando deseamos realizar llamadas asíncronas adicionales una tras otra cuando finalice la anterior.

Entonces, un segundo enfoque es usar Promesas. Una promesa es un objeto que representa un valor que aún podría no existir. Puede establecer devoluciones de llamada, que se invocarán cuando el valor esté listo para leerse.

La diferencia entre las promesas y el enfoque tradicional de devoluciones de llamada es que los métodos asíncronos ahora devuelven sincrónicamente los objetos de promesa, en los que el cliente establece una devolución de llamada. Por ejemplo, código similar usando Promises en AngularJS:

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

Entonces mi pregunta es: ¿hay realmente una diferencia real? La diferencia parece ser puramente sintáctica.

¿Hay alguna razón más profunda para usar una técnica sobre la otra?

Aviv Cohn
fuente
8
Sí: las devoluciones de llamada son solo funciones de primera clase. Las promesas son mónadas que proporcionan un mecanismo componible para encadenar operaciones en valores, y utilizan funciones de orden superior con devoluciones de llamada para proporcionar una interfaz conveniente.
amon
55
@gnat: Dada la calidad relativa de las dos preguntas / respuestas, el voto duplicado debería ser al revés en mi humilde opinión.
Bart van Ingen Schenau

Respuestas:

110

Es justo decir que las promesas son solo azúcar sintáctica. Todo lo que puede hacer con promesas lo puede hacer con devoluciones de llamada. De hecho, la mayoría de las implementaciones prometedoras proporcionan formas de convertir entre los dos cuando lo desee.

La razón profunda por la cual las promesas son a menudo mejores es que son más componibles , lo que significa que combinar múltiples promesas "simplemente funciona", mientras que combinar múltiples devoluciones de llamadas a menudo no lo hace. Por ejemplo, es trivial asignar una promesa a una variable y adjuntar controladores adicionales más adelante, o incluso adjuntar un controlador a un gran grupo de promesas que se ejecutan solo después de que se resuelven todas las promesas. Si bien puede emular estas cosas con devoluciones de llamada, se necesita mucho más código, es muy difícil de hacer correctamente y el resultado final generalmente es mucho menos fácil de mantener.

Una de las formas más grandes (y sutiles) de que las promesas ganen su componibilidad es mediante el manejo uniforme de los valores de retorno y las excepciones no detectadas. Con las devoluciones de llamada, la forma en que se maneja una excepción puede depender completamente de cuál de las muchas devoluciones de llamada anidadas la arrojó, y cuál de las funciones que toman devoluciones de llamada tiene un intento / captura en su implementación. Con las promesas, sabe que una excepción que escapa a una función de devolución de llamada se detectará y pasará al controlador de errores que proporcionó con .error()o .catch().

Para el ejemplo que usted dio de una única devolución de llamada versus una sola promesa, es cierto que no hay una diferencia significativa. Es cuando tienes un billón de devoluciones de llamada versus un billón de promesas que el código basado en promesas tiende a verse mucho mejor.


Aquí hay un intento de un código hipotético escrito con promesas y luego con devoluciones de llamada que deberían ser lo suficientemente complejas como para darle una idea de lo que estoy hablando.

Con promesas:

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 devoluciones de llamada:

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

Puede haber algunas formas inteligentes de reducir la duplicación de código en la versión de devolución de llamada incluso sin promesas, pero todas las que se me ocurren se reducen a implementar algo muy prometedor.

Ixrec
fuente
1
Otra ventaja importante de las promesas es que son susceptibles de una mayor "azúcarificación" con async / wait o una rutina que devuelve los valores prometidos para las yieldpromesas de educación. La ventaja aquí es que puede mezclar estructuras de flujo de control nativas, que pueden variar en la cantidad de operaciones asíncronas que realizan. Agregaré una versión que muestra esto.
acjay
99
La diferencia fundamental entre devoluciones de llamada y promesas es la inversión del control. Con las devoluciones de llamada, su API debe aceptar una devolución de llamada , pero con Promesas, su API debe proporcionar una promesa . Esta es la principal diferencia, y tiene amplias implicaciones para el diseño de API.
cwharris
@ChristopherHarris no estoy seguro de estar de acuerdo. tener un then(callback)método en Promise que acepte una devolución de llamada (en lugar de un método en API que acepte esta devolución de llamada) no tiene que hacer nada con IoC. Promise introduce un nivel de indirección que es útil para la composición, el encadenamiento y el manejo de errores (programación orientada a ferrocarriles en efecto), pero el cliente aún no ejecuta la devolución de llamada, por lo que en realidad no hay ausencia de IoC.
dragan.stepanovic
1
@ dragan.stepanovic Tienes razón, y utilicé la terminología incorrecta. La diferencia es la indirección. Con una devolución de llamada, ya debe saber qué debe hacerse con el resultado. Con una promesa, puedes decidir más tarde.
cwharris