Compreender promessas em JavaScript

Gokul NK 15 de jul Estou fazendo uma promessa de que até o final deste post você saberá melhor o JavaScript Promises.

Eu tive uma espécie de relacionamento de “amor e ódio” com o JavaScript. Mas, no entanto, o JavaScript sempre foi intrigante para mim. Tendo trabalhado em Java e PHP nos últimos 10 anos, o JavaScript parecia muito diferente, mas intrigante. Eu não consegui gastar tempo suficiente em JavaScript e tenho tentado compensar isso ultimamente.

Promessas foi o primeiro tópico interessante que me deparei. Repetidamente eu ouvi pessoas dizendo que Promessas te salvam do inferno Callback. Embora isso possa ter sido um efeito colateral agradável, há mais no Promises e aqui está o que consegui descobrir até agora.

fundo

Quando você começa a trabalhar em JavaScript pela primeira vez, pode ser um pouco frustrante. Você ouvirá algumas pessoas dizendo que o JavaScript é uma linguagem de programação síncrona, enquanto outras afirmam que ele é assíncrono. Você ouve código de bloqueio, código sem bloqueio, padrão de design orientado a eventos, ciclo de vida do evento, pilha de funções, fila de eventos, borbulhamento, polyfill, babel, angular, reactJS, vue JS e várias outras ferramentas e bibliotecas. Não se preocupe. Você não é o primeiro. Existe um termo para isso também. É chamado JavaScript fadiga . Este tweet capta muito bem.

Se você quiser mais detalhes sobre o cansaço do JavaScript, confira o artigo a seguir. Há uma razão para este post ter 42k palmas no Hackernoon 🙂

Como se sente ao aprender JavaScript em 2016
Nenhuma estrutura JavaScript foi criada durante a redação deste artigo. hackernoon.com

JavaScript é uma linguagem de programação síncrona. Mas, graças às funções de callback, podemos fazê-lo funcionar como uma linguagem de programação assíncrona.

Promessas para leigos

Promessas em JavaScript são muito semelhantes às promessas que você faz na vida real. Então, primeiro vamos olhar promessas na vida real.

A definição de uma promessa do dicionário é a seguinte

promessa : substantivo: Garantia de que alguém fará algo ou que uma coisa em particular acontecerá.

Então, o que acontece quando alguém faz uma promessa para você?

  1. Uma promessa lhe dá uma garantia de que algo será feito. Se eles (que fizeram a promessa) farão isso sozinhos ou se o fizerem por outros, é irrelevante. Eles lhe dão uma garantia com base na qual você pode planejar algo.
  2. Uma promessa pode ser mantida ou quebrada.
  3. Quando uma promessa é mantida, você espera algo dessa promessa. Você pode usar a saída de uma promessa para suas ações ou planos adicionais.
  4. Quando uma promessa é quebrada, você gostaria de saber por que a pessoa que fez a promessa não foi capaz de manter seu lado do acordo. Uma vez que você sabe o motivo e tem uma confirmação de que a promessa foi quebrada, você pode planejar o que fazer a seguir ou como lidar com isso.
  5. Na hora de fazer uma promessa, tudo o que temos é apenas uma garantia. Nós não poderemos agir imediatamente. Podemos decidir e formular o que precisa ser feito quando a promessa é mantida (e, portanto, esperamos que o resultado) ou seja quebrada (sabemos a razão e, portanto, podemos planejar uma contingência).
  6. Há uma chance de você não ouvir de volta a pessoa que fez a promessa. Em tais casos, você preferiria manter um limite de tempo. Diga se a pessoa que fez a promessa não voltar para mim em 10 dias, eu considerarei que ele teve alguns problemas e não cumprirá sua promessa. Então, mesmo que a pessoa volte para você depois de 15 dias, isso não importa mais para você, já que você já fez planos alternativos.

Promessas em JavaScript

Como regra geral, para JavaScript, sempre leio a documentação do MDN Web Docs. De todos os recursos, acho que eles fornecem os detalhes mais concisos. Eu li a página Promises do MDSN Web Docs e brinquei com o código para entender.

Existem duas partes para entender as promessas. Criação de promessas e manejo de promessas . Embora a maioria do nosso código geralmente atenda ao tratamento de promessas criadas por outras bibliotecas, um entendimento completo nos ajudará, com certeza. A compreensão da “criação de promessas” é igualmente importante quando você atravessa o estágio iniciante.

Criação de Promessas

Vamos olhar a assinatura para criar uma nova promessa.

 new Promise( /* executor */ function(resolve, reject) { ... } ); 

O construtor aceita uma função chamada executor. Esta função executor aceita dois parâmetros, resolve e reject que por sua vez são funções. As promessas geralmente são usadas para facilitar o manuseio de operações assíncronas ou código de bloqueio, exemplos para os quais são operações de arquivos, chamadas de API, chamadas de banco de dados, chamadas de E / S etc. O início dessas operações assíncronas ocorre dentro da função do executor . Se as operações assíncronas forem bem-sucedidas, o resultado esperado será retornado, chamando a função de resolve pelo criador da promessa. Da mesma forma, se houve algum erro inesperado, as razões são transmitidas chamando a função de reject .

Agora que sabemos como criar uma promessa. Vamos criar uma promessa simples para o nosso entendimento.

 var keepsHisWord; 
keepsHisWord = true;
promise1 = new Promise (função (resolver, rejeitar) {
if (keepsHisWord) {
resolve ("O homem gosta de manter sua palavra");
} outro {
rejeitar ("O homem não quer manter sua palavra");
}
});
console.log (promise1);

Toda promessa tem um estado e valor

Como essa promessa será resolvida imediatamente, não poderemos inspecionar o estado inicial da promessa. Então, vamos apenas criar uma nova promessa que levará algum tempo para ser resolvida. A maneira mais fácil para isso é usar a função setTimeOut .

 promise2 = new Promise (função (resolver, rejeitar) { 
setTimeout (function () {
resolver({
mensagem: "O homem gosta de manter sua palavra",
código: "aManKeepsHisWord"
});
}, 10 * 1000);
});
console.log (promise2);

O código acima apenas cria uma promessa que será resolvida incondicionalmente após 10 segundos. Assim, podemos verificar o estado da promessa até que ela seja resolvida.

estado de promessa até que seja resolvido ou rejeitado

Quando os dez segundos acabarem, a promessa é resolvida. PromiseStatus e PromiseValue são atualizados de acordo. Como você pode ver, atualizamos a função resolve para podermos passar um Objeto JSON em vez de uma string simples. Isso é apenas para mostrar que podemos passar outros valores também na função de resolve .

Uma promessa que resolve após 10 segundos com um objeto JSON como valor retornado

Agora vamos olhar para uma promessa que irá rejeitar. Vamos apenas modificar um pouco a promessa para isso.

 keepsHisWord = false; 
promise3 = new Promise (função (resolver, rejeitar) {
if (keepsHisWord) {
resolve ("O homem gosta de manter sua palavra");
} outro {
rejeite ("O homem não quer manter sua palavra");
}
});
console.log (promise3);

Como isso criará um navegador chrome de rejeição não manipulado, será exibido um erro. Você pode ignorá-lo por enquanto. Voltaremos a isso mais tarde.

rejeições em promessas

Como podemos ver, o PromiseStatus pode ter três valores diferentes. pending resolved ou rejected Quando a promessa for criada, o PromiseStatus estará no status pending e terá o PromiseValue como undefined até que a promessa seja resolved ou rejected. Quando uma promessa está em estados resolved ou rejected , uma promessa é dita como settled. Assim, uma promessa geralmente transita do estado pendente para o estado estabelecido.

Agora que sabemos como as promessas são criadas, podemos ver como podemos usar ou lidar com promessas. Isso vai de mãos dadas com a compreensão do objeto Promise .

Compreender promessas Objeto

Conforme a documentação do MDN

O objeto Promise representa a conclusão final (ou falha) de uma operação assíncrona e seu valor resultante.

Promise objeto Promise possui métodos estáticos e métodos de prototype methods estáticos no objeto Promise podem ser aplicados independentemente, enquanto os prototype methods precisam ser aplicados nas instâncias do objeto Promise . Lembrando que tanto os métodos normais quanto os protótipos retornam uma Promise torna muito mais fácil entender as coisas.

Métodos de Protótipo

Vamos primeiro começar com os prototype methods Existem três deles. Apenas para reiterar, lembre-se de que todos esses métodos podem ser aplicados em uma instância do objeto Promise e todos esses métodos retornam uma promessa por vez. Todos os métodos a seguir atribuem manipuladores para diferentes transições de estado de uma promessa. Como vimos anteriormente, quando uma Promise é criada, ela está em estado pending . Um ou mais dos três métodos a seguir serão executados quando uma promessa for definida com base no fulfilled ou rejected .

Promise.prototype.catch(onRejected)

Promise.prototype.then(onFulfilled, onRejected)

Promise.prototype.finally(onFinally)

A imagem abaixo mostra o fluxo para .then e .catch métodos. Uma vez que eles retornam uma Promise eles podem ser encadeados novamente, o que também é mostrado na imagem. Se, .finally for declarado para uma promessa, ele será executado sempre que uma promessa for settled independentemente de ser cumprida ou rejeitada. Como Konstantin Rouda apontou, há um suporte limitado para finalmente, portanto, verifique antes de usar isso.

De: https://mdn.mozillademos.org/files/15911/promises.png

Aqui está uma pequena história. Você é uma criança de escola e você pede a sua mãe por um telefone. Ela diz: "Vou comprar um telefone para o final deste mês".

Vamos ver como ficará em JavaScript se a promessa for executada no final do mês.

 var momsPromise = new Promise (função (resolver, rejeitar) { 
momsSavings = 20000;
priceOfPhone = 60000;
if (momsSavings> priceOfPhone) {
resolver({
marca: "iphone",
modelo: "6s"
});
} outro {
rejeitar ("Não temos poupanças suficientes. Economizemos um pouco mais");
}
});
 momsPromise.then (function (value) { 
console.log ("Viva esse telefone como presente", JSON.stringify (valor));
});
 momsPromise.catch (function (reason) { 
console.log ("Mamãe não pode me comprar o telefone porque", razão);
});
 momsPromise.finally (function () { 
console.log (
"Irrespecitve de saber se minha mãe pode me comprar um telefone ou não, eu ainda a amo"
);
});

A saída para isso será.

Mães falharam promessa.

Se mudarmos o valor de momsSavings para 200000, então a mãe poderá presentear o filho. Nesse caso, a saída será

mãe mantém sua promessa.

Deixe-nos usar o chapéu de alguém que consome essa biblioteca. Nós estamos zombando da saída e da natureza para que possamos ver como usar e pegar efetivamente.

Desde .then pode atribuir tanto onFulfilled, onRejected handlers , em vez de escrever separado .then e .catch que poderíamos ter feito o mesmo com com .then Teria parecia abaixo.

 momsPromise.then ( 
function (value) {
console.log ("Viva esse telefone como presente", JSON.stringify (valor));
}
função (razão) {
console.log ("Mamãe não pode me comprar o telefone porque", razão);
}
);

Mas para a legibilidade do código, acho melhor mantê-los separados.

Para garantir que possamos executar todas essas amostras nos navegadores em geral ou no chrome em específico, estou certificando-me de que não temos dependências externas em nossas amostras de código. Para entender melhor os tópicos adicionais, vamos criar uma função que retornará uma promessa que será resolvida ou rejeitada aleatoriamente para que possamos testar vários cenários. Para entender o conceito de funções assíncronas, introduzamos um atraso aleatório também em nossa função. Como precisaremos de números aleatórios, vamos primeiro criar uma função aleatória que retornará um número aleatório entre xe y.

 function getRandomNumber (start = 1, end = 10) { 
// funciona quando ambos start, end são> = 1 e end> start
return parseInt (Math.random () * end)% (início de término + 1) + início;
}

Vamos criar uma função que retornará uma promessa para nós. Vamos chamar nossa função promiseTRRARNOSG que é um apelido para promiseThatResolvesRandomlyAfterRandomNumnberOfSecondsGenerator . Esta função criará uma promessa que resolverá ou rejeitará após um número aleatório de segundos entre 2 e 10. Para randomizar rejeição e resolução, criaremos um número aleatório entre 1 e 10. Se o número aleatório gerado for maior, 5 resolveremos o problema. promessa, senão nós vamos rejeitá-lo.

 function getRandomNumber (start = 1, end = 10) { 
// funciona quando ambos, início e fim, são> = 1
return (parseInt (Math.random () * end)% (fim - início + 1)) + início;
}
 var promiseTRRARNOSG = ( promiseThatResolvesRandomlyAfterRandomNumnberOfSecondsGenerator = function () { 
return new Promise (função (resolver, rejeitar) {
deixe randomNumberOfSeconds = getRandomNumber (2, 10);
setTimeout (function () {
let randomiseResolving = getRandomNumber (1, 10);
if (randomiseResolving> 5) {
resolver({
randomNumberOfSeconds: randomNumberOfSeconds,
randomiseResolving: randomiseResolving
});
} outro {
rejeitar({
randomNumberOfSeconds: randomNumberOfSeconds,
randomiseResolving: randomiseResolving
});
}
}, randomNumberOfSeconds * 1000);
});
});
 var testProimse = promiseTRRARNOSG (); 
testProimse.then (function (value) {
console.log ("Valor quando a promessa é resolvida:", valor);
});
testProimse.catch (function (reason) {
console.log ("Razão quando a promessa é rejeitada:", razão);
});
 // Vamos percorrer e criar dez promessas diferentes usando a função para ver alguma variação. Alguns serão resolvidos e alguns serão rejeitados. 
 para (i = 1; i <= 10; i ++) { 
deixe promessa = promiseTRRARNOSG ();
promise.then (function (value) {
console.log ("Valor quando a promessa é resolvida:", valor);
});
promise.catch (function (reason) {
console.log ("Razão quando a promessa é rejeitada:", razão);
});
}

Atualize a página do navegador e execute o código no console para ver as diferentes saídas para resolve e reject cenários. No futuro, veremos como podemos criar várias promessas e verificar suas saídas sem precisar fazer isso.

Métodos estáticos

Existem quatro métodos estáticos no objeto Promise .

Os dois primeiros são métodos ou atalhos de ajuda. Eles ajudam você a criar promessas resolvidas ou rejeitadas facilmente.

Promise.reject(reason)

Ajuda você a criar uma promessa rejeitada.

 var promise3 = Promise.reject ("Não está interessado"); 
promise3.then (function (value) {
console.log ("Isso não será executado, pois é uma promessa resolvida. O valor resolvido é", value);
});
promise3.catch (function (reason) {
console.log ("Isto corre como é uma promessa rejeitada. A razão é", razão);
});

Promise.resolve(value)

Ajuda você a criar uma promessa resolvida.

 var promise4 = Promise.resolve (1); 
promise4.then (function (value) {
console.log ("Isso será executado como é uma promessa resovled. O valor resolvido é", valor);
});
promise4.catch (function (reason) {
console.log ("Isto não será executado como é uma promessa resolvida", razão);
});

Em um sidenote, uma promessa pode ter vários manipuladores. Então você pode atualizar o código acima para

 var promise4 = Promise.resolve (1); 
promise4.then (function (value) {
console.log ("Isso será executado como é uma promessa resovled. O valor resolvido é", valor);
});
promise4.then (function (value) {
console.log ("Isso também será executado, pois vários manipuladores podem ser adicionados. Imprimindo duas vezes o valor resolvido que é", valor * 2);
});
promise4.catch (function (reason) {
console.log ("Isto não será executado como é uma promessa resolvida", razão);
});

E a saída será semelhante.

Os próximos dois métodos ajudam a processar um conjunto de promessas. Quando você está lidando com múltiplas promessas, é melhor criar uma série de promessas primeiro e depois fazer a ação necessária sobre o conjunto de promessas. Para entender esses métodos, não seremos capazes de usar nossa prática promiseTRRARNOSG como promiseTRRARNOSG , pois é muito aleatório. É melhor ter algumas promessas deterministas para que possamos entender o comportamento. Vamos criar duas funções. Um que resolverá após n segundos e um que será rejeitado após n segundos.

 var promiseTRSANSG = (promiseThatResolvesAfterNSecondsGenerator = função ( 
n = 0
) {
return new Promise (função (resolver, rejeitar) {
setTimeout (function () {
resolver({
resolvedAfterNSeconds: n
});
}, n * 1000);
});
});
var promiseTRJANSG = (promiseThatRejectsAfterNSecondsGenerator = função (
n = 0
) {
return new Promise (função (resolver, rejeitar) {
setTimeout (function () {
rejeitar({
rejectedAfterNSeconds: n
});
}, n * 1000);
});
});

Agora vamos usar essas funções auxiliares para entender Promise.All

Promise.All

Conforme a documentação do MDN

O Promise.all(iterable) retorna um único Promise que resolve quando todas as promessas no argumento iterable foram resolvidas ou quando o argumento iterável não contém promessas. Ele rejeita com a razão da primeira promessa que rejeita.

Caso 1 : Quando todas as promessas forem resolvidas. Este é o cenário mais utilizado.

 console.time ("Promise.All"); 
var promisesArray = [];
promisesArray.push (promiseTRSANSG (1));
promisesArray.push (promiseTRSANSG (4));
promisesArray.push (promiseTRSANSG (2));
var handleAllPromises = Promise.all (promisesArray);
handleAllPromises.then (function (values) {
console.timeEnd ("Promise.All");
console.log ("Todas as promessas são resolvidas", valores);
});
handleAllPromises.catch (function (reason) {
console.log ("Uma das promessas falhou com o seguinte motivo", razão);
});

Todas as promessas resolvidas.

Há duas observações importantes que precisamos fazer em geral a partir da saída.

Primeiro : A terceira promessa que leva 2 segundos termina antes da segunda promessa que leva 4 segundos. Mas como você pode ver na saída, a ordem das promessas é mantida nos valores.

Segundo : eu adicionei um temporizador de console para descobrir quanto tempo Promise.All toma. Se as promessas foram executadas em sequência, deveria ter sido 1 + 4 + 2 = 7 segundos no total. Mas do nosso temporizador, vimos que leva apenas 4 segundos. Esta é uma prova de que todas as promessas foram executadas em paralelo.

Caso 2: quando não há promessas. Eu acho que este é o menos utilizado.

 console.time ("Promise.All"); 
var promisesArray = [];
promisesArray.push (1);
promisesArray.push (4);
promisesArray.push (2);
var handleAllPromises = Promise.all (promisesArray);
handleAllPromises.then (function (values) {
console.timeEnd ("Promise.All");
console.log ("Todas as promessas são resolvidas", valores);
});
handleAllPromises.catch (function (reason) {
console.log ("Uma das promessas falhou com o seguinte motivo", razão);
});

Como não há promessas na matriz, a promessa de retorno é resolvida.

Caso 3: Rejeita com o motivo da primeira promessa que rejeita.

 console.time ("Promise.All"); 
var promisesArray = [];
promisesArray.push (promiseTRSANSG (1));
promisesArray.push (promiseTRSANSG (5));
promisesArray.push (promiseTRSANSG (3));
promisesArray.push (promiseTRJANSG (2));
promisesArray.push (promiseTRSANSG (4));
var handleAllPromises = Promise.all (promisesArray);
handleAllPromises.then (function (values) {
console.timeEnd ("Promise.All");
console.log ("Todas as promessas são resolvidas", valores);
});
handleAllPromises.catch (function (reason) {
console.timeEnd ("Promise.All");
console.log ("Uma das promessas falhou com o seguinte motivo", razão);
});

Execução interrompida após a primeira rejeição

Promise.race

Conforme a documentação do MDN

O Promise.race(iterable) retorna uma promessa que resolve ou rejeita assim que uma das promessas do iterável é resolvida ou rejeitada, com o valor ou a razão dessa promessa.

Caso 1: Uma das promessas resolve primeiro.

 console.time ("Promise.race"); 
var promisesArray = [];
promisesArray.push (promiseTRSANSG (4));
promisesArray.push (promiseTRSANSG (3));
promisesArray.push (promiseTRSANSG (2));
promisesArray.push (promiseTRJANSG (3));
promisesArray.push (promiseTRSANSG (4));
var promisesRace = Promise.race (promisesArray);
promisesRace.then (function (values) {
console.timeEnd ("Promise.race");
console.log ("A promessa jejuada resolvida", valores);
});
promisesRace.catch (function (reason) {
console.timeEnd ("Promise.race");
console.log ("A promessa mais rápida rejeitada com o seguinte motivo", razão);
});

resolução mais rápida

Todas as promessas são executadas em paralelo. A terceira promessa é resolvida em 2 segundos. Assim que isso for feito, a promessa retornada pelo Promise.race será resolvida.

Caso 2: Uma das promessas rejeita primeiro.

 console.time ("Promise.race"); 
var promisesArray = [];
promisesArray.push (promiseTRSANSG (4));
promisesArray.push (promiseTRSANSG (6));
promisesArray.push (promiseTRSANSG (5));
promisesArray.push (promiseTRJANSG (3));
promisesArray.push (promiseTRSANSG (4));
var promisesRace = Promise.race (promisesArray);
promisesRace.then (function (values) {
console.timeEnd ("Promise.race");
console.log ("A promessa jejuada resolvida", valores);
});
promisesRace.catch (function (reason) {
console.timeEnd ("Promise.race");
console.log ("A promessa mais rápida rejeitada com o seguinte motivo", razão);
});

rejeição mais rápida

Todas as promessas são executadas em paralelo. A quarta promessa rejeitada em 3 segundos. Assim que isso for feito, a promessa retornada pelo Promise.race é rejeitada.

Eu escrevi todos os métodos de exemplo para que eu possa testar vários cenários e testes podem ser executados no próprio navegador. Essa é a razão pela qual você não vê nenhuma chamada de API, operações de arquivo ou chamadas de banco de dados nos exemplos. Embora todos esses exemplos sejam da vida real, você precisa de um esforço adicional para configurá-los e testá-los. Considerando que o uso das funções de atraso fornece cenários semelhantes sem o ônus da configuração adicional. Você pode facilmente brincar com os valores para ver e verificar diferentes cenários. Você pode usar a combinação dos promiseTRJANSG , promiseTRSANSG e promiseTRRARNOSG para simular cenários suficientes para uma compreensão completa das promessas. O uso dos métodos console.time antes e depois dos blocos relevantes também nos ajudará a identificar facilmente se as promessas são executadas paralelamente ou sequencialmente. Deixe-me saber se você tem outros cenários interessantes ou se eu perdi alguma coisa. Se você quiser todas as amostras de código em um único lugar, confira esta essência.

Bluebird tem alguns recursos interessantes como

  1. Promise.prototype.timeout
  2. Promise.
  3. Promise.promisify

Vamos discutir isso em um post separado.

Eu também estarei escrevendo mais um post sobre meus aprendizados de assíncrono e aguardo.

Antes de fechar, gostaria de listar todas as regras que segui para manter minha mente sã em torno de promessas.

Thumb Rules para usar promessas

  1. Use promessas sempre que estiver usando código assíncrono ou de bloqueio.
  2. resolve mapas para then e reject mapas a serem catch para todos os fins práticos.
  3. Certifique-se de escrever tanto .catch e .then métodos para todas as promessas.
  4. Se algo precisa ser feito em ambos os casos, use .finally
  5. Nós só temos uma chance de mudar cada promessa.
  6. Podemos adicionar vários manipuladores a uma única promessa.
  7. O tipo de retorno de todos os métodos no objeto Promise sejam eles métodos estáticos ou protótipos, é novamente uma Promise
  8. Em Promise.all a ordem das promessas é mantida em valores variáveis, independentemente de qual promessa foi resolvida pela primeira vez.