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

92

Al realizar una programación asíncrona de un solo hilo, existen dos técnicas principales con las que estoy familiarizado. El más común es el uso de devoluciones de llamada. Eso significa pasar a la función que actúa de forma asíncrona una función de devolución de llamada como un parámetro. Cuando finalice la operación asíncrona, se llamará la devolución de llamada.

Algún código jQuery 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 ser desordenado y altamente anidado cuando queremos hacer llamadas asíncronas adicionales una tras otra cuando termina la anterior.

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

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

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

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

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

    
pregunta Aviv Cohn 12.11.2015 - 23:26

1 respuesta

109

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

La razón profunda por la que las promesas a menudo son mejores es que son más compositivas , lo que significa que la combinación de múltiples promesas "simplemente funciona", mientras que la combinación de múltiples devoluciones de llamadas no es así. 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 hayan resuelto todas las promesas. Si bien es posible 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 suele ser mucho menos mantenible.

Una de las mayores (y más sutiles) formas en que las promesas obtienen su capacidad de composición 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 llamadas anidadas se lanzó, 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 de 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 dio de una devolución de llamada única frente a una promesa única, es cierto que no hay una diferencia significativa. Es cuando tienes un millón de devoluciones de llamada frente a un millón de promesas que el código basado en la promesa tiende a verse mucho mejor.

Aquí se intenta un código hipotético escrito con promesas y luego con devoluciones de llamadas que deberían ser lo suficientemente complejas como para darte 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 devoluciones de llamada incluso sin promesas, pero todas las que puedo pensar se reducen a implementar algo muy prometedor.

    
respondido por el Ixrec 12.11.2015 - 23:37

Lea otras preguntas en las etiquetas