Existe realmente uma diferença fundamental entre callbacks e Promises?

92

Ao fazer uma programação assíncrona de encadeamento ú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 age de forma assíncrona como uma função de retorno de chamada. Quando a operação assíncrona terminar, o retorno de chamada será chamado.

Algum código típico de jQuery projetado dessa 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 terminar.

Portanto, uma segunda abordagem é usar o Promises. Um Promise é 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 tradicional de retorno de chamada é que os métodos assíncronos agora retornam de forma síncrona os objetos Promise, nos quais o cliente define um retorno de chamada. Por exemplo, código semelhante usando Promises em AngularJS:

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

Então, minha pergunta é: há 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?

    
por Aviv Cohn 12.11.2015 / 23:26
fonte

1 resposta

109

É justo dizer que as 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 promessas de implementação fornece maneiras de converter entre os dois sempre que você quiser.

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

Uma das maiores (e mais sutis) maneiras pelas quais as promessas ganham sua composibilidade é por manipulação uniforme de valores de retorno e exceções não identificadas. Com os retornos de chamada, como uma exceção é manipulada pode depender inteiramente de quais dos muitos retornos de chamada aninhados a executaram, e quais das funções que usam retornos de chamada têm um try / catch 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 passada para o manipulador de erros fornecido com .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 promessa tende a parecer muito mais agradável.

Aqui está uma tentativa de algum código hipotético escrito com promessas e, em seguida, com callbacks que devem ser apenas 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 callbacks mesmo sem promessas, mas todas as que posso pensar se resumem à implementação de algo muito promissor.

    
por 12.11.2015 / 23:37
fonte