Imagine que você é um cantor top e os fãs perguntam dia e noite pela sua próxima música.
Para obter algum alívio, você promete enviá-lo quando for publicado. Você dá uma lista aos seus fãs. Eles podem preencher seus endereços de e-mail para que, quando a música estiver disponível, todos os assinantes a recebam instantaneamente. E mesmo que algo dê muito errado, digamos, um incêndio no estúdio, para que você não consiga publicar a música, eles ainda serão avisados.
Todos ficam felizes: você, porque o povo não te aglomera mais, e os fãs, porque não vão perder a música.
Esta é uma analogia da vida real para coisas que frequentemente temos na programação:
Um “código de produção” que faz alguma coisa e leva tempo. Por exemplo, algum código que carrega os dados em uma rede. Isso é um “cantor”.
Um “código consumidor” que quer o resultado do “código produzido” quando estiver pronto. Muitas funções podem precisar desse resultado. Esses são os “fãs”.
Uma promessa é um objeto JavaScript especial que vincula o “código de produção” e o “código de consumo”. Nos termos da nossa analogia: esta é a “lista de assinaturas”. O “código de produção” leva o tempo necessário para produzir o resultado prometido, e a “promessa” disponibiliza esse resultado para todo o código inscrito quando estiver pronto.
A analogia não é muito precisa, porque as promessas do JavaScript são mais complexas do que uma simples lista de assinaturas: elas têm recursos e limitações adicionais. Mas está tudo bem para começar.
A sintaxe do construtor para um objeto de promessa é:
deixe promessa = new Promise(function(resolver, rejeitar) { // executor (o código de produção, "singer") });
A função passada para new Promise
é chamada de executor . Quando new Promise
é criada, o executor é executado automaticamente. Ele contém o código de produção que deve eventualmente produzir o resultado. Nos termos da analogia acima: o executor é o “cantor”.
Seus argumentos resolve
e reject
são retornos de chamada fornecidos pelo próprio JavaScript. Nosso código está apenas dentro do executor.
Quando o executor obtiver o resultado, seja cedo ou tarde, não importa, ele deverá chamar um destes callbacks:
resolve(value)
— se o trabalho for concluído com sucesso, com resultado value
.
reject(error)
— se ocorreu um erro, error
é o objeto de erro.
Então, para resumir: o executor é executado automaticamente e tenta executar um trabalho. Ao finalizar a tentativa, ele chama resolve
se foi bem sucedido ou reject
se houve erro.
O objeto promise
retornado pelo new Promise
possui estas propriedades internas:
state
- inicialmente "pending"
e depois muda para "fulfilled"
quando resolve
é chamada ou "rejected"
quando reject
é chamada.
result
— inicialmente undefined
, depois muda para value
quando resolve(value)
é chamado ou error
quando reject(error)
é chamado.
Portanto, o executor eventualmente move promise
para um destes estados:
Mais tarde veremos como os “fãs” podem subscrever estas mudanças.
Aqui está um exemplo de um construtor de promessa e uma função executora simples com “produção de código” que leva tempo (via setTimeout
):
deixe promessa = new Promise(function(resolver, rejeitar) { // a função é executada automaticamente quando a promessa é construída // após 1 segundo sinaliza que o trabalho foi concluído com o resultado "concluído" setTimeout(() => resolve("concluído"), 1000); });
Podemos ver duas coisas executando o código acima:
O executor é chamado automática e imediatamente (por new Promise
).
O executor recebe dois argumentos: resolve
e reject
. Essas funções são pré-definidas pelo mecanismo JavaScript, portanto não precisamos criá-las. Só devemos ligar para um deles quando estivermos prontos.
Após um segundo de “processamento”, o executor chama resolve("done")
para produzir o resultado. Isso altera o estado do objeto promise
:
Esse foi um exemplo de conclusão de trabalho bem-sucedida, uma “promessa cumprida”.
E agora um exemplo do executor rejeitando a promessa com erro:
deixe promessa = new Promise(function(resolver, rejeitar) { // após 1 segundo sinaliza que o trabalho foi concluído com um erro setTimeout(() => rejeitar(new Error("Opa!")), 1000); });
A chamada para reject(...)
move o objeto de promessa para o estado "rejected"
:
Para resumir, o executor deve realizar um trabalho (geralmente algo que leva tempo) e então chamar resolve
ou reject
para alterar o estado do objeto de promessa correspondente.
Uma promessa que é resolvida ou rejeitada é chamada de “resolvida”, em oposição a uma promessa inicialmente “pendente”.
Pode haver apenas um único resultado ou um erro
O executor deve chamar apenas uma resolve
ou uma reject
. Qualquer mudança de estado é definitiva.
Todas as outras chamadas de resolve
e reject
são ignoradas:
deixe promessa = new Promise(function(resolver, rejeitar) { resolver("concluído"); rejeitar(novo erro("…")); //ignorado setTimeout(() => resolve("…")); //ignorado });
A ideia é que um trabalho realizado pelo executor possa ter apenas um resultado ou um erro.
Além disso, resolve
/ reject
espera apenas um argumento (ou nenhum) e irá ignorar argumentos adicionais.
Rejeitar com objetos Error
Caso algo dê errado, o executor deverá chamar reject
. Isso pode ser feito com qualquer tipo de argumento (assim como resolve
). Mas é recomendado usar objetos Error
(ou objetos que herdam de Error
). O raciocínio para isso logo se tornará aparente.
Chamando imediatamente resolve
/ reject
Na prática, um executor geralmente faz algo de forma assíncrona e chama resolve
/ reject
depois de algum tempo, mas não é necessário. Também podemos chamar resolve
ou reject
imediatamente, assim:
deixe promessa = new Promise(function(resolver, rejeitar) { // não dedicamos nosso tempo para fazer o trabalho resolver(123); //fornece imediatamente o resultado: 123 });
Por exemplo, isso pode acontecer quando começamos a fazer um trabalho, mas depois vemos que tudo já foi concluído e armazenado em cache.
Isso é bom. Imediatamente temos uma promessa resolvida.
O state
e result
são internos
O state
das propriedades e result
do objeto Promise são internos. Não podemos acessá-los diretamente. Podemos usar os métodos .then
/ .catch
/ .finally
para isso. Eles são descritos abaixo.
Um objeto Promise serve como um elo entre o executor (o “código produtor” ou “cantor”) e as funções consumidoras (os “fãs”), que receberão o resultado ou erro. As funções de consumo podem ser registradas (assinadas) usando os métodos .then
e .catch
.
O mais importante e fundamental é .then
.
A sintaxe é:
promessa.então( function(resultado) { /* lidar com um resultado bem-sucedido */ }, function(error) { /* tratar um erro */ } );
O primeiro argumento de .then
é uma função executada quando a promessa é resolvida e recebe o resultado.
O segundo argumento de .then
é uma função executada quando a promessa é rejeitada e recebe o erro.
Por exemplo, aqui está uma reação a uma promessa resolvida com sucesso:
deixe promessa = new Promise(function(resolver, rejeitar) { setTimeout(() => resolve("pronto!"), 1000); }); // resolve executa a primeira função em .then promessa.então( resultado => alerta(resultado), // mostra "pronto!" depois de 1 segundo erro => alerta(erro) // não roda );
A primeira função foi executada.
E no caso de rejeição, a segunda:
deixe promessa = new Promise(function(resolver, rejeitar) { setTimeout(() => rejeitar(new Error("Opa!")), 1000); }); // rejeitar executa a segunda função em .then promessa.então( resultado => alerta(resultado), // não executa error => alert(error) // mostra "Erro: Opa!" depois de 1 segundo );
Se estivermos interessados apenas em conclusões bem-sucedidas, poderemos fornecer apenas um argumento de função para .then
:
deixe promessa = nova promessa(resolver => { setTimeout(() => resolve("pronto!"), 1000); }); promessa.então(alerta); // mostra "pronto!" depois de 1 segundo
Se estivermos interessados apenas em erros, podemos usar null
como primeiro argumento: .then(null, errorHandlingFunction)
. Ou podemos usar .catch(errorHandlingFunction)
, que é exatamente o mesmo:
deixe promessa = new Promise((resolver, rejeitar) => { setTimeout(() => rejeitar(new Error("Opa!")), 1000); }); // .catch(f) é o mesmo que promessa.then(null, f) promessa.catch(alerta); // mostra "Erro: Opa!" depois de 1 segundo
A chamada .catch(f)
é um análogo completo de .then(null, f)
, é apenas uma abreviação.
Assim como há uma cláusula finally
em um try {...} catch {...}
normal, há finally
promessas.
A chamada .finally(f)
é semelhante a .then(f, f)
no sentido de que f
é executado sempre, quando a promessa é cumprida: seja ela resolvida ou rejeitada.
A ideia de finally
é configurar um manipulador para realizar a limpeza/finalização após a conclusão das operações anteriores.
Por exemplo, parar os indicadores de carregamento, fechar conexões desnecessárias, etc.
Pense nisso como um finalizador de festa. Não importa se a festa foi boa ou ruim, quantos amigos estavam nela, ainda precisamos (ou pelo menos deveríamos) fazer uma limpeza depois dela.
O código pode ficar assim:
nova promessa((resolver, rejeitar) => { /* faça algo que leve tempo e depois chame resolver ou talvez rejeitar */ }) // é executado quando a promessa é cumprida, não importa com sucesso ou não .finalmente(() => parar o indicador de carregamento) // para que o indicador de carregamento seja sempre parado antes de prosseguirmos .then(resultado => mostrar resultado, err => mostrar erro)
Observe que finally(f)
não é exatamente um alias de then(f,f)
.
Existem diferenças importantes:
Um manipulador finally
não tem argumentos. finally
não sabemos se a promessa foi bem-sucedida ou não. Tudo bem, pois nossa tarefa normalmente é realizar procedimentos de finalização “gerais”.
Por favor, dê uma olhada no exemplo acima: como você pode ver, o manipulador finally
não tem argumentos e o resultado da promessa é tratado pelo manipulador seguinte.
Um manipulador finally
“passa” o resultado ou erro para o próximo manipulador adequado.
Por exemplo, aqui o resultado é passado finally
para then
:
nova promessa((resolver, rejeitar) => { setTimeout(() => resolve("valor"), 2000); }) .finally(() => alert("Promise ready")) // dispara primeiro .then(resultado => alerta(resultado)); // <-- .then mostra "valor"
Como você pode ver, o value
retornado pela primeira promessa é passado finally
para o próximo then
.
Isso é muito conveniente, porque finally
não se destina a processar um resultado promissor. Como dito, é um lugar para fazer uma limpeza genérica, não importa qual seja o resultado.
E aqui está um exemplo de erro, para vermos como ele finally
foi transmitido para catch
:
nova promessa((resolver, rejeitar) => { lançar novo erro("erro"); }) .finally(() => alert("Promise ready")) // dispara primeiro .catch(err => alerta(err)); // <-- .catch mostra o erro
Um manipulador finally
também não deve retornar nada. Se isso acontecer, o valor retornado será ignorado silenciosamente.
A única exceção a esta regra é quando um manipulador finally
gera um erro. Então esse erro vai para o próximo manipulador, em vez de qualquer resultado anterior.
Para resumir:
Um manipulador finally
não obtém o resultado do manipulador anterior (não possui argumentos). Em vez disso, esse resultado é passado para o próximo manipulador adequado.
Se um manipulador finally
retornar algo, ele será ignorado.
Quando finally
ocorre um erro, a execução vai para o manipulador de erros mais próximo.
Esses recursos são úteis e fazem as coisas funcionarem da maneira certa se finally
usarmos como deveriam ser usados: para procedimentos genéricos de limpeza.
Podemos anexar manipuladores às promessas estabelecidas
Se uma promessa estiver pendente, os manipuladores .then/catch/finally
aguardam seu resultado.
Às vezes, pode ser que uma promessa já esteja resolvida quando adicionamos um manipulador a ela.
Nesse caso, esses manipuladores são executados imediatamente:
// a promessa é resolvida imediatamente após a criação deixe promessa = new Promise(resolve => resolve("pronto!")); promessa.então(alerta); // feito! (aparece agora)
Observe que isso torna as promessas mais poderosas do que o cenário de “lista de assinaturas” da vida real. Se o cantor já lançou sua música e então uma pessoa se inscreve na lista de assinaturas, provavelmente não receberá aquela música. As inscrições na vida real devem ser feitas antes do evento.
As promessas são mais flexíveis. Podemos adicionar manipuladores a qualquer momento: se o resultado já estiver lá, eles simplesmente executam.
A seguir, veremos exemplos mais práticos de como as promessas podem nos ajudar a escrever código assíncrono.
Temos a função loadScript
para carregar um script do capítulo anterior.
Aqui está a variante baseada em retorno de chamada, apenas para nos lembrar disso:
function loadScript(src, retorno de chamada) { deixe script = document.createElement('script'); script.src=src; script.onload = () => retorno de chamada (nulo, script); script.onerror = () => callback(new Error(`Erro de carregamento de script para ${src}`)); documento.head.append(script); }
Vamos reescrevê-lo usando Promises.
A nova função loadScript
não exigirá retorno de chamada. Em vez disso, ele criará e retornará um objeto Promise que será resolvido quando o carregamento for concluído. O código externo pode adicionar manipuladores (funções de assinatura) usando .then
:
função carregarScript(src) { retornar nova Promessa(função(resolver, rejeitar) { deixe script = document.createElement('script'); script.src=src; script.onload = () => resolver(script); script.onerror = () => rejeitar(new Error(`Erro de carregamento de script para ${src}`)); documento.head.append(script); }); }
Uso:
deixe promessa = loadScript("https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"); promessa.então( script => alerta(`${script.src} está carregado!`), erro => alerta(`Erro: ${error.message}`) ); promessa.then(script => alert('Outro manipulador...'));
Podemos ver imediatamente alguns benefícios em relação ao padrão baseado em retorno de chamada:
Promessas | Retornos de chamada |
---|---|
As promessas nos permitem fazer as coisas na ordem natural. Primeiro, executamos loadScript(script) e, .then escrevemos o que fazer com o resultado. | Devemos ter uma função callback à nossa disposição ao chamar loadScript(script, callback) . Em outras palavras, devemos saber o que fazer com o resultado antes que loadScript seja chamado. |
Podemos .then uma promessa quantas vezes quisermos. Cada vez, adicionamos um novo “fã”, uma nova função de assinatura, à “lista de assinaturas”. Mais sobre isso no próximo capítulo: Encadeamento de promessas. | Só pode haver um retorno de chamada. |
Portanto, as promessas nos proporcionam melhor fluxo de código e flexibilidade. Mas há mais. Veremos isso nos próximos capítulos.
Qual é a saída do código abaixo?
deixe promessa = new Promise(function(resolver, rejeitar) { resolver(1); setTimeout(() => resolver(2), 1000); }); promessa.então(alerta);
A saída é: 1
.
A segunda chamada para resolve
é ignorada, porque apenas a primeira chamada para reject/resolve
é levada em consideração. Outras chamadas serão ignoradas.
A função interna setTimeout
usa retornos de chamada. Crie uma alternativa baseada em promessas.
A função delay(ms)
deve retornar uma promessa. Essa promessa deve ser resolvida após ms
milissegundos, para que possamos adicionar .then
a ela, assim:
atraso de função (ms) { //seu código } delay(3000).then(() => alert('executa após 3 segundos'));
atraso de função (ms) { retornar nova Promessa(resolver => setTimeout(resolver, ms)); } delay(3000).then(() => alert('executa após 3 segundos'));
Observe que nesta tarefa resolve
é chamada sem argumentos. Não devolvemos nenhum valor de delay
, apenas garantimos o atraso.
Reescreva a função showCircle
na solução da tarefa Círculo animado com retorno de chamada para que retorne uma promessa em vez de aceitar um retorno de chamada.
O novo uso:
mostrarCírculo(150, 150, 100).then(div => { div.classList.add('mensagem-bola'); div.append("Olá, mundo!"); });
Pegue a solução da tarefa Círculo animado com retorno de chamada como base.
Abra a solução em uma sandbox.