As promessas não são apenas retornos de chamada?

430

Estou desenvolvendo JavaScript há alguns anos e não entendo nada sobre promessas.

Parece que tudo o que faço é mudar:

api(function(result){
    api2(function(result2){
        api3(function(result3){
             // do work
        });
    });
});

Para o qual eu poderia usar uma biblioteca como assíncrona, de qualquer maneira, com algo como:

api().then(function(result){
     api2().then(function(result2){
          api3().then(function(result3){
               // do work
          });
     });
});

Qual é mais código e menos legível. Eu não ganhei nada aqui, de repente também não é magicamente 'plano'. Sem mencionar a necessidade de converter as coisas em promessas.

Então, qual é o grande alarido sobre promessas aqui?

Benjamin Gruenbaum
fonte
11
Sobre o tema : há um artigo muito informativo sobre Promises em HTML5Rocks: html5rocks.com/en/tutorials/es6/promises
ComFreek
2
Para você, a resposta que você aceitou é a mesma lista antiga dos benefícios triviais que não são o ponto principal das promessas e nem me convenceram a usar promessas: /. O que me convenceu a uso promessas foi o aspecto DSL como descrito na resposta de Oscar
Esailija
@ Esailija bem, seu leet falar me convenceu. Aceitei a outra resposta, apesar de achar que a de Bergi também levanta alguns pontos realmente bons (e diferentes).
Benjamin Gruenbaum
@Esailija "O que me convenceu a usar promessas foi o aspecto DSL, conforme descrito na resposta do Oscar" << O que é "DSL"? e qual é o "aspecto DSL" ao qual você está se referindo?
monsto
1
@monsto: DSL: Domain Specific Language, uma linguagem projetada propositadamente para ser usada em um subconjunto específico de um sistema (por exemplo, SQL ou ORM para conversar com o banco de dados, regex para encontrar padrões, etc.). Nesse contexto, o "DSL" é a API da Promise que, se você estruturar seu código da maneira que Oscar fez, é quase como um açúcar sintático que complementa o JavaScript para abordar o contexto específico de operações assíncronas. As promessas criam alguns idiomas que os transformam em quase uma linguagem projetada para permitir que o programador compreenda mais facilmente o fluxo mental um tanto esquivo desse tipo de estrutura.
Michael Ekoka

Respostas:

631

Promessas não são retornos de chamada. Uma promessa representa o resultado futuro de uma operação assíncrona . É claro que, ao escrevê-las como você faz, você obtém pouco benefício. Mas se você as escrever da maneira como elas devem ser usadas, poderá escrever código assíncrono de uma maneira que se assemelhe ao código síncrono e seja muito mais fácil de seguir:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
});

Certamente, não muito menos código, mas muito mais legível.

Mas este não é o fim. Vamos descobrir os verdadeiros benefícios: E se você quisesse verificar algum erro em alguma das etapas? Seria um inferno fazer isso com retornos de chamada, mas com promessas, é um pedaço de bolo:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
});

Praticamente o mesmo que um try { ... } catchbloco.

Melhor ainda:

api().then(function(result){
    return api2();
}).then(function(result2){
    return api3();
}).then(function(result3){
     // do work
}).catch(function(error) {
     //handle any error that may occur before this point
}).then(function() {
     //do something whether there was an error or not
     //like hiding an spinner if you were performing an AJAX request.
});

E melhor ainda: E se esses 3 chamadas para api, api2, api3poderia correr simultaneamente (por exemplo, se fossem chamadas AJAX), mas você precisava esperar para os três? Sem promessas, você deve criar algum tipo de contador. Com promessas, usando a notação ES6, é outro pedaço de bolo e bem arrumado:

Promise.all([api(), api2(), api3()]).then(function(result) {
    //do work. result is an array contains the values of the three fulfilled promises.
}).catch(function(error) {
    //handle the error. At least one of the promises rejected.
});

Espero que você veja as promessas sob uma nova luz agora.

Oscar Paz
fonte
124
Eles realmente não deveriam tê-lo nomeado como "Promessa". "Futuro" é pelo menos 100x melhor.
Pacerier
12
@ Pacerier porque o Future não foi contaminado pelo jQuery?
Esailija 31/01
5
Padrão alternativo (dependendo do que é desejado: api (). Then (api2) .then (api3) .then (doWork); Ou seja, se as funções api2 / api3 receberem entrada da última etapa e retornarem novas promessas, elas serão pode ser encadeado sem empacotamento extra, isto é, eles compõem #
Dtipson
1
E se houver operações assíncronas em api2e api3? o último .thenseria chamado apenas quando essas operações assíncronas estivessem concluídas?
NiCk Newman
8
Por que você me marcou? Eu apenas corrigi a gramática um pouco. Eu não sou um especialista em JS. :)
Scott Arciszewski
169

Sim, promessas são retornos de chamada assíncronos. Eles não podem fazer nada que os retornos de chamada não possam fazer, e você enfrenta os mesmos problemas com assincronia e com retornos simples.

No entanto, as promessas são mais do que apenas retornos de chamada. Eles são uma abstração muito poderosa, permitem códigos funcionais mais limpos e melhores com clichê menos propenso a erros.

Então, qual é a ideia principal?

Promessas são objetos que representam o resultado de uma única computação (assíncrona). Eles resolvem esse resultado apenas uma vez. Existem algumas coisas que isso significa:

Promessas implementam um padrão de observador:

  • Você não precisa conhecer os retornos de chamada que usarão o valor antes que a tarefa seja concluída.
  • Em vez de esperar retornos de chamada como argumentos para suas funções, você pode facilmente returnum objeto Promise
  • A promessa armazenará o valor e você poderá adicionar um retorno de chamada de forma transparente sempre que desejar. Será chamado quando o resultado estiver disponível. "Transparência" implica que, quando você tem uma promessa e adiciona um retorno de chamada, não faz diferença para o seu código se o resultado já chegou - a API e os contratos são os mesmos, simplificando muito o cache / memória.
  • Você pode adicionar vários retornos de chamada facilmente

As promessas são encadeadas ( monádicas , se você quiser ):

  • Se você precisar transformar o valor que uma promessa representa, você mapeie uma função de transformação sobre a promessa e receba de volta uma nova promessa que represente o resultado transformado. Você não pode obter de forma síncrona o valor para usá-lo de alguma forma, mas pode facilmente elevar a transformação no contexto da promessa. Não há retornos de chamada padrão.
  • Se você deseja encadear duas tarefas assíncronas, pode usar o .then() método É necessário um retorno de chamada para ser chamado com o primeiro resultado e retorna uma promessa para o resultado da promessa que o retorno de chamada retorna.

Parece complicado? Hora de um exemplo de código.

var p1 = api1(); // returning a promise
var p3 = p1.then(function(api1Result) {
    var p2 = api2(); // returning a promise
    return p2; // The result of p2 …
}); // … becomes the result of p3

// So it does not make a difference whether you write
api1().then(function(api1Result) {
    return api2().then(console.log)
})
// or the flattened version
api1().then(function(api1Result) {
    return api2();
}).then(console.log)

O achatamento não ocorre magicamente, mas você pode fazê-lo facilmente. Para o seu exemplo fortemente aninhado, o equivalente (quase) seria

api1().then(api2).then(api3).then(/* do-work-callback */);

Se ver o código desses métodos ajuda a entender, aqui está uma lib de promessa mais básica em algumas linhas .

Qual é o grande alarido sobre promessas?

A abstração Promise permite uma composição de funções muito melhor. Por exemplo, ao lado dethen de encadeamento, a allfunção cria uma promessa para o resultado combinado de várias promessas de espera paralela.

Por último, mas não menos importante, as promessas vêm com o tratamento de erros integrado. O resultado do cálculo pode ser que a promessa seja cumprida com um valor ou seja rejeitada por uma razão. Todas as funções de composição lidam com isso automaticamente e propagam erros em cadeias de promessas, para que você não precise se preocupar com isso explicitamente em todos os lugares - em contraste com uma implementação simples de retorno de chamada. No final, você pode adicionar um retorno de chamada de erro dedicado para todas as exceções ocorridas.

Sem mencionar a necessidade de converter as coisas em promessas.

Isso é bastante trivial, na verdade, com boas bibliotecas de promessas, consulte Como converter uma API de retorno de chamada existente em promessas?

Bergi
fonte
oi Bergi, você gostaria de acrescentar algo interessante a essa pergunta do SO? stackoverflow.com/questions/22724883/...
Sebastien Lorber
1
@Sebastien: Eu não sei muito sobre Scala (ainda), e eu só podia repetir o que Benjamin disse :-)
Bergi
3
Apenas uma pequena observação: você não pode usar .then(console.log), pois console.log depende do contexto do console. Dessa forma, causará um erro de chamada ilegal. Use console.log.bind(console)ou x => console.log(x)para vincular o contexto.
Tamas Hegedus
3
@hege_hegedus: Existem ambientes onde os consolemétodos já estão vinculados. E, é claro, eu apenas disse que os dois ninhos têm exatamente o mesmo comportamento, não que nenhum deles funcione:
P
1
Isso foi ótimo. Era disso que eu precisava: menos código e mais interpretação. Obrigado.
Adam Patterson
21

Além das respostas já estabelecidos, com ES6 seta funções Promises transformar de um pequeno anão azul modestamente brilhando diretamente em uma gigante vermelha. Isso está prestes a entrar em colapso em uma supernova:

api().then(result => api2()).then(result2 => api3()).then(result3 => console.log(result3))

Como o oligofren apontou, sem argumentos entre chamadas de API, você não precisa das funções de invólucro anônimo:

api().then(api2).then(api3).then(r3 => console.log(r3))

E, finalmente, se você deseja atingir um nível supermassivo de buraco negro, podem ser esperadas promessas:

async function callApis() {
    let api1Result = await api();
    let api2Result = await api2(api1Result);
    let api3Result = await api3(api2Result);

    return api3Result;
}
John Weisz
fonte
9
"com ES6 arrow funções Promises transformar de uma pequena estrela azul modestamente brilhando em linha reta em uma gigante vermelha que está prestes a ruir em uma supernova." Tradução: Combinando ES6 arrow funções com promessas é incrível :)
user3344977
3
Isso faz Promessas parecer uma catástrofe cósmica, que não acho que tenha sido sua intenção.
Michael McGinnis
Se você não estiver usando os argumentos nos apiXmétodos, assim como você pode ignorar as funções de seta completamente: api().then(api2).then(api3).then(r3 => console.log(r3)).
Oligofren #
@MichaelMcGinnis - O impacto benéfico do Promises em um inferno de retorno de chamada é como uma supernova explodindo em um canto escuro do espaço.
John Weisz
Eu sei que você quer dizer isso poeticamente, mas as promessas estão muito longe de "supernova". A violação da lei monádica ou a falta de suporte para casos de uso mais poderosos, como cancelamento ou retorno de vários valores, vêm à mente.
Dmitri Zaitsev
15

Além das respostas impressionantes acima, mais 2 pontos podem ser adicionados:

1. Diferença semântica:

As promessas já podem ser resolvidas após a criação. Isso significa que eles garantem condições e não eventos . Se eles já estiverem resolvidos, a função resolvida passada para ele ainda será chamada.

Por outro lado, os retornos de chamada manipulam eventos. Portanto, se o evento em que você tiver interesse tiver ocorrido antes do registro de retorno, o retorno não será chamado.

2. Inversão de controle

Os retornos de chamada envolvem inversão de controle. Quando você registra uma função de retorno de chamada com qualquer API, o tempo de execução do Javascript armazena a função de retorno de chamada e a chama a partir do loop de eventos quando estiver pronta para ser executada.

Consulte O loop de eventos Javascript para obter uma explicação.

Com Promessas , o controle reside no programa de chamada. O método .then () pode ser chamado a qualquer momento se armazenarmos o objeto de promessa.

dww
fonte
1
Não sei por que, mas isso parece ser uma resposta melhor.
radiantshaw
13

Além das outras respostas, a sintaxe do ES2015 combina perfeitamente com as promessas, reduzindo ainda mais o código padrão:

// Sequentially:
api1()
  .then(r1 => api2(r1))
  .then(r2 => api3(r2))
  .then(r3 => {
      // Done
  });

// Parallel:
Promise.all([
    api1(),
    api2(),
    api3()
]).then(([r1, r2, r3]) => {
    // Done
});
Duncan Luk
fonte
5

Promessas não são retornos de chamada, ambos são idiomas de programação que facilitam a programação assíncrona. O uso de um estilo de programação assíncrono / aguardado, usando corotinas ou geradores que retornam promessas, pode ser considerado um terceiro idioma desse tipo. Uma comparação desses idiomas entre diferentes linguagens de programação (incluindo Javascript) está aqui: https://github.com/KjellSchubert/promise-future-task

Kjell Schubert
fonte
5

Não, não mesmo.

Retornos de chamada são simplesmente funções em JavaScript chamada que devem ser chamadas e executadas após a conclusão de outra função. Então como isso acontece?

Na verdade, no JavaScript, as funções são consideradas como objetos e, portanto, como todos os outros objetos, até as funções podem ser enviadas como argumentos para outras funções . O caso de uso mais comum e genérico em que se pode pensar é a função setTimeout () em JavaScript.

As promessas nada mais são do que uma abordagem muito mais improvisada de manipulação e estruturação de código assíncrono, em comparação com fazer o mesmo com retornos de chamada.

A promessa recebe dois retornos de chamada na função construtora: resolver e rejeitar. Essas promessas de retorno internas nos fornecem controle refinado sobre o tratamento de erros e casos de sucesso. O retorno de chamada de resolução é usado quando a execução da promessa é realizada com êxito e o retorno de chamada de rejeição é usado para manipular os casos de erro.

Ayush Jain
fonte
2

Nenhuma promessa é apenas invólucro nos retornos de chamada

exemplo Você pode usar promessas nativas de javascript com o nó js

my cloud 9 code link : https://ide.c9.io/adx2803/native-promises-in-node

/**
* Created by dixit-lab on 20/6/16.
*/

var express = require('express');
var request = require('request');   //Simplified HTTP request client.


var app = express();

function promisify(url) {
    return new Promise(function (resolve, reject) {
    request.get(url, function (error, response, body) {
    if (!error && response.statusCode == 200) {
        resolve(body);
    }
    else {
        reject(error);
    }
    })
    });
}

//get all the albums of a user who have posted post 100
app.get('/listAlbums', function (req, res) {
//get the post with post id 100
promisify('http://jsonplaceholder.typicode.com/posts/100').then(function (result) {
var obj = JSON.parse(result);
return promisify('http://jsonplaceholder.typicode.com/users/' + obj.userId + '/albums')
})
.catch(function (e) {
    console.log(e);
})
.then(function (result) {
    res.end(result);
}
)

})


var server = app.listen(8081, function () {

var host = server.address().address
var port = server.address().port

console.log("Example app listening at http://%s:%s", host, port)

})


//run webservice on browser : http://localhost:8081/listAlbums
Apoorv
fonte
1

As promessas JavaScript realmente usam funções de retorno de chamada para determinar o que fazer depois que uma promessa foi resolvida ou rejeitada, portanto, ambas não são fundamentalmente diferentes. A principal idéia por trás do Promises é receber retornos de chamada - especialmente retornos aninhados, nos quais você deseja executar uma espécie de ações, mas seria mais legível.

Hamid Shoja
fonte
0

Visão geral das promessas:

Em JS, podemos envolver operações assíncronas (por exemplo, chamadas de banco de dados, chamadas AJAX) em promessas. Normalmente, queremos executar alguma lógica adicional nos dados recuperados. As promessas de JS têm funções de manipulador que processam o resultado das operações assíncronas. As funções do manipulador podem até ter outras operações assíncronas dentro delas, que podem contar com o valor das operações assíncronas anteriores.

Uma promessa sempre tem um dos três seguintes estados:

  1. pendente: estado inicial de toda promessa, nem cumprida nem rejeitada.
  2. cumprida: A operação foi concluída com sucesso.
  3. rejeitado: a operação falhou.

Uma promessa pendente pode ser resolvida / preenchida ou rejeitada com um valor. Em seguida, os seguintes métodos manipuladores que recebem retornos de chamada como argumentos são chamados:

  1. Promise.prototype.then() : Quando a promessa for resolvida, o argumento de retorno de chamada dessa função será chamado.
  2. Promise.prototype.catch() : Quando a promessa é rejeitada, o argumento de retorno de chamada dessa função será chamado.

Embora a habilidade dos métodos acima obtenha argumentos de retorno de chamada, eles são muito superiores ao uso apenas de retorno de chamada. Aqui está um exemplo que esclarecerá muito:

Exemplo

function createProm(resolveVal, rejectVal) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            if (Math.random() > 0.5) {
                console.log("Resolved");
                resolve(resolveVal);
            } else {
                console.log("Rejected");
                reject(rejectVal);
            }
        }, 1000);
    });
}

createProm(1, 2)
    .then((resVal) => {
        console.log(resVal);
        return resVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
        return resVal + 2;
    })
    .catch((rejectVal) => {
        console.log(rejectVal);
        return rejectVal + 1;
    })
    .then((resVal) => {
        console.log(resVal);
    })
    .finally(() => {
        console.log("Promise done");
    });

  • A função createProm cria promessas que são resolvidas ou rejeitadas com base em um Nr aleatório após 1 segundo
  • Se a promessa for resolvida, o primeiro thenmétodo é chamado e o valor resolvido é passado como argumento do retorno de chamada
  • Se a promessa for rejeitada, o primeiro catchmétodo é chamado e o valor rejeitado é passado como argumento
  • Os métodos catche thenretornam promessas, é por isso que podemos encadeá-los. Eles envolvem qualquer valor retornado Promise.resolvee qualquer valor jogado (usando a throwpalavra - chave) Promise.reject. Portanto, qualquer valor retornado é transformado em promessa e, novamente, podemos chamar uma função de manipulador.
  • As cadeias de promessa nos fornecem controle mais refinado e melhor visão geral do que retornos de chamada aninhados. Por exemplo, o catchmétodo lida com todos os erros que ocorreram antes do catchmanipulador.
Willem van der Veen
fonte