Existe realmente uma diferença fundamental entre retornos de chamada e promessas?

94

Ao fazer programação assíncrona de thread único, há duas técnicas principais com as quais estou familiarizado. O mais comum é usar retornos de chamada. Isso significa passar para a função que atua de forma assíncrona uma função de retorno de chamada como parâmetro. Quando a operação assíncrona for concluída, o retorno de chamada será chamado.

Algum jQuerycódigo típico projetado desta maneira:

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

No entanto, esse tipo de código pode ficar confuso e altamente aninhado quando queremos fazer chamadas assíncronas adicionais uma após a outra quando a anterior termina.

Portanto, uma segunda abordagem está usando o Promises. Uma promessa é um objeto que representa um valor que talvez ainda não exista. Você pode definir retornos de chamada, que serão chamados quando o valor estiver pronto para ser lido.

A diferença entre o Promises e a abordagem de retorno de chamada tradicional é que agora os métodos assíncronos retornam objetos Promise de forma síncrona, na qual o cliente define um retorno de chamada. Por exemplo, código semelhante usando Promises no AngularJS:

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

Então, minha pergunta é: existe realmente uma diferença real? A diferença parece ser puramente sintática.

Existe alguma razão mais profunda para usar uma técnica sobre a outra?

Aviv Cohn
fonte
8
Sim: retornos de chamada são apenas funções de primeira classe. Promessas são mônadas que fornecem um mecanismo de composição para encadear operações em valores e usam funções de ordem superior com retornos de chamada para fornecer uma interface conveniente.
amon
5
@gnat: Dada a qualidade relativa das duas perguntas / respostas, o voto duplicado deve ser o contrário de IMHO.
Bart van Ingen Schenau 13/11/2015

Respostas:

110

É justo dizer que promessas são apenas açúcar sintático. Tudo o que você pode fazer com promessas que você pode fazer com retornos de chamada. De fato, a maioria das implementações promissoras fornece formas de conversão entre as duas quando você quiser.

A razão profunda pela qual as promessas costumam ser melhores é que elas são mais compostáveis , o que significa aproximadamente que combinar várias promessas "simplesmente funciona", enquanto a combinação de vários retornos de chamada geralmente não funciona. Por exemplo, é trivial atribuir uma promessa a uma variável e anexar manipuladores adicionais a ela mais tarde, ou até anexar um manipulador a um grande grupo de promessas que são executadas somente após todas as promessas serem resolvidas. Embora você possa emular essas coisas com retornos de chamada, é preciso muito mais código, é muito difícil de executar corretamente, e o resultado final geralmente é muito menos sustentável.

Uma das maiores (e mais sutis) maneiras pelas quais as promessas ganham sua composição é através do tratamento uniforme dos valores de retorno e das exceções não capturadas. Com os retornos de chamada, a maneira como uma exceção é tratada pode depender inteiramente de qual dos muitos retornos de chamada aninhados a lançou e qual das funções que recebem retornos de chamada tem uma tentativa / captura em sua implementação. Com promessas, você sabe que uma exceção que escapa de uma função de retorno de chamada será capturada e transmitida ao manipulador de erros que você forneceu .error()ou .catch().

Para o exemplo que você deu de um único retorno de chamada versus uma única promessa, é verdade que não há diferença significativa. É quando você tem um zilhão de retornos de chamada versus um zilhão de promessas que o código baseado em promessas tende a parecer muito melhor.


Aqui está uma tentativa de algum código hipotético escrito com promessas e, em seguida, com retornos de chamada que devem ser complexos o suficiente para lhe dar uma idéia do que estou falando.

Com promessas:

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

Com retornos de chamada:

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

Pode haver algumas maneiras inteligentes de reduzir a duplicação de código na versão de retorno de chamada, mesmo sem promessas, mas todas as que posso pensar se resumem a implementar algo muito parecido com promessa.

Ixrec
fonte
1
Outra grande vantagem das promessas é que elas são passíveis de "sugarificação" adicional com async / waitit ou uma corotina que devolve os valores prometidos para as yieldpromessas. A vantagem aqui é que você tem a capacidade de misturar estruturas de fluxo de controle nativo, que podem variar em quantas operações assíncronas são executadas. Vou adicionar uma versão que mostra isso.
acjay
9
A diferença fundamental entre retornos de chamada e promessas é a inversão de controle. Com retornos de chamada, sua API deve aceitar um retorno de chamada , mas com Promessas, sua API deve fornecer uma promessa . Essa é a principal diferença e tem amplas implicações no design da API.
Cwharris
@ChristopherHarris não tem certeza se eu concordaria. Ter um then(callback)método no Promise que aceita um retorno de chamada (em vez de um método na API que aceita esse retorno de chamada) não precisa fazer nada com a IoC. O Promise introduz um nível de indireção que é útil para composição, encadeamento e tratamento de erros (programação orientada a transporte ferroviário em vigor), mas o retorno de chamada ainda não é executado pelo cliente, portanto, não há realmente ausência de IoC.
23818 Dragan.stepanovic 17/02/19
1
@ dragan.stepanovic Você está certo, e eu usei a terminologia errada. A diferença é a indireção. Com um retorno de chamada, você já deve saber o que precisa ser feito com o resultado. Com uma promessa, você pode decidir mais tarde.
Cwharris