Visualização de promessas e Async / Await



Bom dia amigos

Apresento a você a tradução do artigo "JavaScript Visualized: Promises & Async / Await", de Lydia Hallie.

Você encontrou um código JavaScript que ... não funciona conforme o esperado? Quando as funções são executadas em uma ordem arbitrária e imprevisível, ou são executadas com um atraso. Uma das principais tarefas das promessas é otimizar a execução de funções.

Minha curiosidade insaciável e noites sem dormir valeram a pena - graças a eles, criei várias animações. É hora de falar sobre promessas: como elas funcionam, por que devem ser usadas e como são feitas.

Introdução


Ao escrever o código JS, geralmente precisamos lidar com tarefas que dependem de outras tarefas. Suponha que queremos obter uma imagem, compactá-la, aplicar um filtro e salvá-la.

Primeiro de tudo, precisamos obter uma imagem. Para fazer isso, use a função getImage. Depois de carregar a imagem, passamos para a função resizeImage. Depois de comprimir a imagem, podemos aplicar um filtro a ele usando a função applyFilter. Após a compactação e a aplicação do filtro, salvamos a imagem e informamos o usuário sobre o sucesso.

Como resultado, obtemos o seguinte:



Hmm ... Você notou alguma coisa? Apesar de tudo funcionar, ele não parece o melhor caminho. Temos muitas funções de retorno de chamada aninhadas que dependem de retornos de chamada anteriores. Isso se chama inferno de retorno de chamada e torna muito difícil ler e manter o código.

Felizmente, hoje temos promessas.



Sintaxe da promessa


Promessas foram introduzidas no ES6. Em muitos manuais, você pode ler o seguinte:

Uma promessa (promessa) é um valor que é cumprido ou rejeitado no futuro.

Sim ... Explicação. Ao mesmo tempo, isso me fez considerar promessas como algo estranho, vago, algum tipo de mágica. O que eles são realmente?

Podemos criar uma promessa com um construtor Promiseque aceita uma função de retorno de chamada como argumento. Legal, vamos tentar:



Espere, o que está voltando aqui?

PromiseÉ um objeto que contém status ( [[PromiseStatus]]) e valor ( [[PromiseValue]]). No exemplo acima, o valor [[PromiseStatus]]é pendinge o valor da promessa é undefined.

Não se preocupe, você não precisa interagir com esse objeto, nem pode acessar as propriedades [[PromiseStatus]]e [[PromiseValue]]. No entanto, essas propriedades são muito importantes ao trabalhar com promessas.

PromiseStatus ou o status de uma promessa pode assumir um dos três valores:

  • fulfilled: resolved (). ,
  • rejected: rejected (). -
  • pending: , pending (, )

Parece ótimo, mas quando uma promessa obtém os status indicados? E por que o status importa?

No exemplo acima, passamos uma Promisefunção simples de retorno de chamada para o construtor () => {}. Esta função leva dois argumentos. O valor do primeiro argumento, geralmente chamado resolveou res, é o método invocado quando a promessa é executada. O valor do segundo argumento, geralmente chamado rejectou rej, é o método chamado quando a promessa é rejeitada quando algo dá errado.



Vamos ver o que é produzido no console ao chamar os métodos resolvee reject:



Legal! Agora sabemos como nos livrar do status pendinge do significado undefined. O status da promessa ao chamar o método resolveé fulfilledquando reject:rejected.

[[PromiseValue]]ou o valor da promessa é o valor que passamos aos métodos resolveou rejectcomo argumento.

Curiosidade: Jake Archibald, depois de ler este artigo, apontou para um bug no Chrome, que fulfilledretornou resolved.



Ok, agora sabemos como trabalhar com o objeto Promise. Mas para que é usado?

Na introdução, dei um exemplo no qual obtemos uma imagem, compactamos, aplicamos um filtro e salvamos. Então tudo terminou com um inferno de retorno de chamada.

Felizmente, as promessas ajudam a lidar com isso. Reescrevemos o código para que cada função retorne uma promessa.

Se a imagem foi carregada, cumprimos uma promessa. Caso contrário, se ocorrer um erro, rejeite a promessa:



Vamos ver o que acontece quando esse código é executado no terminal:



Legal! Promis retorna com dados analisados ​​("analisados"), como esperávamos.

Mas ... o que vem depois? Não estamos interessados ​​no assunto da promis, estamos interessados ​​nos seus dados. Existem 3 métodos internos para obter um valor Promise:

  • .then(): chamado após realizar uma promessa
  • .catch(): chamado após a rejeição da promessa
  • .finally(): sempre chamado, após a execução e após a rejeição de uma promessa



O método .thenleva o valor passado ao método resolve:



O método .catchleva o valor passado ao método reject:



Finalmente, obtivemos o valor desejado. Nós podemos fazer qualquer coisa com esse valor.

Quando estamos confiantes no cumprimento ou rejeição de uma promessa, você pode escrever Promise.resolvetanto Promise.rejectcom o valor apropriado.



Essa é a sintaxe que será usada nos exemplos a seguir.



O resultado .thené o valor da promessa (ou seja, esse método também retorna a promessa). Isso significa que podemos usar o quanto .thenfor necessário: o resultado do anterior .thené passado como argumento para o próximo .then.



Em getImagepodemos usar vários .thenpara transferir a imagem processada para a próxima função.



Essa sintaxe parece muito melhor do que a escada de funções de retorno de chamada aninhadas.



Microtasks e tarefas (macro)


Bem, agora sabemos como criar promessas e como extrair valores delas. Adicione algum código ao nosso script e execute-o novamente:



Primeiro, ele é exibido no console Start!. Isso é normal, pois temos a primeira linha de código console.log('Start!'). O segundo valor exibido no console End!não é o valor da promessa concluída. O valor da promessa é exibido por último. Por que isso aconteceu?

Aqui vemos o poder das promessas. Embora o JS seja de thread único, podemos tornar o código assíncrono Promise.

Onde mais podemos observar comportamento assíncrono? Alguns métodos criados no navegador, como setTimeout, podem simular assincronia.

Direita No loop de eventos (Loop de Eventos), existem dois tipos de filas: a fila de tarefas (macro) ou simplesmente tarefas (fila de tarefas (macro), fila de tarefas) e a fila de microtasks ou apenas microtasks (fila microtask, microtasks).

O que se aplica a cada um deles? Em suma, então:

  • Tarefas da macro: setTimeout, setInterval, setImmediate
  • Microtasks: process.nextTick, retorno de chamada Promise, filaMicrotask

Vemos Promisena lista de microtasks. Quando o Promisemétodo é executado e chamado then(), catch()ou finally(), a função de retorno de chamada com o método é adicionada à fila de microtask. Isso significa que o retorno de chamada com o método não é executado imediatamente, o que torna o código JS assíncrono.

Quando é o método then(), catch()ou finally()se executou? As tarefas no loop de eventos têm a seguinte prioridade:

  1. Primeiro, as funções na pilha de chamadas são executadas. Os valores retornados por essas funções são removidos da pilha.
  2. Depois que a pilha é liberada, as microtasks são colocadas uma após a outra (as microtasks podem retornar outras microtasks, criando um ciclo interminável de microtasks).
  3. Depois que a pilha e a fila de microtask são liberadas, o loop de eventos verifica se há macros. As tarefas de macro são colocadas na pilha, executadas e excluídas.



Considere um exemplo:

  • Task1: Uma função que é adicionada à pilha imediatamente, por exemplo, por uma chamada no código.
  • Task2, Task3, Task4: Mikrozadachi exemplo thenProMIS ou tarefa adicionada via queueMicrotask.
  • Task5, Task6: tarefas macro, por exemplo, setTimeoutousetImmediate



Primeiro Task1retorna um valor e é removido da pilha. Em seguida, o mecanismo verifica se há micro-tarefas na fila correspondente. Depois de adicionar e remover subsequentemente as microtarefas da pilha, o mecanismo verifica as tarefas de macro, que também são adicionadas à pilha e removidas após o retorno dos valores.

Chega de palavras. Vamos escrever o código.



Nesse código, temos uma setTimeouttarefa macro e uma micro tarefa .then. Execute o código e veja o que é exibido no console.

Nota: no exemplo acima, eu uso métodos como console.log, setTimeoute Promise.resolve. Todos esses métodos são internos, portanto, eles não aparecem no rastreamento da pilha - não se surpreenda quando você não os encontrar nas ferramentas de solução de problemas do navegador.

Na primeira linha, temosconsole.log. É adicionado à pilha e exibido no console Start!. Depois disso, esse método é removido da pilha e o mecanismo continua a analisar o código.



O mecanismo alcança setTimeout, que é adicionado à pilha. Esse método é um método de navegador interno: sua função de retorno de chamada ( () => console.log('In timeout')) é adicionada à API da Web e existe antes que o timer seja acionado. Apesar do contador de timer ser 0, o retorno de chamada ainda é colocado primeiro na WebAPI e depois na fila de tarefas macro: setTimeout- esta é uma tarefa macro.



Em seguida, o mecanismo atinge o método Promise.resolve(). Este método é adicionado à pilha e, em seguida, executado com um valor Promise. Seu retorno de chamada thené colocado na fila de microtask.



Finalmente, o mecanismo atinge o segundo método console.log(). É imediatamente empurrado para a pilha, é enviado para o consoleEnd!, o método é removido da pilha e o mecanismo continua.



O mecanismo "vê" que a pilha está vazia. Verificando a fila de tarefas. Está localizado lá then. É empurrado para a pilha, o valor da promessa é exibido no console: neste caso, uma sequência Promise!.



O mecanismo vê que a pilha está vazia. Ele "olha" na fila de microtasks. Ela também está vazia.

É hora de verificar a fila de tarefas macro: está lá setTimeout. É empurrado para a pilha e retorna um método console.log(). Uma string é enviada ao console 'In timeout!'. setTimeouté removido da pilha.



Feito. Agora tudo se encaixou, certo?



Assíncrono / aguardar


O ES7 introduziu uma nova maneira de trabalhar com código assíncrono em JS. Usando as palavras asynce awaitpodemos criar uma função assíncrona que retorna implicitamente uma promessa. Mas ... como fazemos isso?

Anteriormente, discutimos como criar explicitamente um objeto Promise: using new Promise(() => {}), Promise.resolveou Promise.reject.

Em vez disso, podemos criar uma função assíncrona que retorna implicitamente o objeto especificado. Isso significa que não precisamos mais criar manualmente Promise.



O fato de uma função assíncrona retornar implicitamente uma promessa é, obviamente, ótimo, mas o poder dessa função é totalmente manifestado ao usar uma palavra-chave await.awaitfaz com que a função assíncrona aguarde a promessa (seu valor) ser concluída. Para obter o valor da promessa realizada, devemos atribuir à variável o valor esperado (esperado) da promessa.

Acontece que podemos atrasar a execução de uma função assíncrona? Ótimo, mas ... o que isso significa?

Vamos ver o que acontece quando você executa o seguinte código:







A princípio o motor vê console.log. Este método é colocado na pilha e exibido no console Before function!.



Então a função assíncrona é chamada myFunc(), seu código é executado. Na primeira linha deste código, chamamos a segunda console.logcom uma linha 'In function!'. Este método é adicionado à pilha, seu valor é exibido no console e é removido da pilha.



O código da função é executado a seguir. Na segunda linha, temos uma palavra-chave await.

A primeira coisa que acontece aqui é a execução do valor esperado: neste caso, a função one. É empurrado para a pilha e retorna uma promessa. Depois que a promessa foi concluída e a função oneretornou o valor, o mecanismo vê await.

Depois disso, a execução da função assíncrona é atrasada. A execução do corpo da função é suspensa, o código restante é executado como uma microtask.



Após o atraso na execução da função assíncrona, o mecanismo retorna à execução do código em um contexto global.



Depois de todo o código ter sido executado em um contexto global, o loop de eventos verifica se há microtarefas e detecta myFunc(). myFunc()empurrado para a pilha e executado.

A variável resobtém o valor da promessa executada retornada pela função one. Chamamos console.logcom o valor da variável res: uma string One!neste caso. One!É exibido no console.

Feito. Observe a diferença entre a função assíncrona e o método thenpromis? Palavra-chaveawaitadia a execução de uma função assíncrona. Se usássemos then, o corpo da Promise continuaria funcionando.



Acabou bem detalhado. Não se preocupe se você se sentir inseguro ao trabalhar com promessas. Leva algum tempo para se acostumar com eles. Isso é comum a todas as técnicas para trabalhar com código assíncrono em JS.

Consulte também "Visualização do trabalho dos trabalhadores do serviço" .

Obrigado pelo seu tempo. Espero que tenha sido bem gasto.

All Articles