Vamos explorar esse conceito e aprender muito sobre JavaScript no processo.

Você está pronto? Vamos.

O que é código assíncrono?

Por design, JavaScript é uma linguagem de programação síncrona. Isso significa que quando o código é executado, o JavaScript começa na parte superior do arquivo e percorre o código linha por linha, até que seja concluído.

O resultado dessa decisão de design é que apenas uma coisa pode acontecer por vez.

Você pode pensar nisso como se estivesse fazendo malabarismo com seis pequenas bolas. Enquanto você está fazendo malabarismos, suas mãos estão ocupadas e não podem lidar com mais nada.

É o mesmo com JavaScript: uma vez que o código está em execução, ele fica totalmente ocupado com esse código. Chamamos esse tipo de código síncrono bloqueio. Porque está efetivamente bloqueando a execução de outro código.

Voltemos ao exemplo do malabarismo. O que aconteceria se você quisesse adicionar outra bola? Em vez de seis bolas, você queria fazer malabarismos com sete bolas. Isso pode ser um problema.

Você não quer parar de fazer malabarismos, porque é muito divertido. Mas você também não pode ir buscar outra bola, porque isso significaria que você teria que parar.

A solução? Delegue o trabalho a um amigo ou familiar. Eles não estão fazendo malabarismos, então podem ir pegar a bola para você e depois jogá-la no seu malabarismo quando sua mão estiver livre e você estiver pronto para adicionar outra bola no meio do malabarismo.

Isso é código assíncrono. JavaScript é delegar o trabalho a outra pessoa e, em seguida, cuidar de seu próprio negócio. Então, quando estiver pronto, receberá os resultados do trabalho.

Quem está fazendo o outro trabalho?

Tudo bem, então sabemos que o JavaScript é síncrono e lento. Ele não quer fazer todo o trabalho sozinho, então o transfere para outra coisa.

Mas quem é essa entidade misteriosa que funciona para JavaScript? E como ele é contratado para trabalhar com JavaScript?

Bem, vamos dar uma olhada em um exemplo de código assíncrono.

const logName = () => {   console.log("Han")}setTimeout(logName, 0)console.log("Hi there")

A execução desse código resulta na seguinte saída no console:

// in consoleHi thereHan

Tudo bem. O que está acontecendo?

Acontece que a maneira como fazemos trabalho em JavaScript é usar funções e APIs específicas do ambiente. E isso é uma fonte de grande confusão em JavaScript.

JavaScript sempre é executado em um ambiente.

Freqüentemente, esse ambiente é o navegador. Mas também pode estar no servidor com NodeJS. Mas qual é a diferença?

A diferença – e isso é importante – é que o navegador e o servidor (NodeJS), em termos de funcionalidade, não são equivalentes. Muitas vezes são semelhantes, mas não são iguais.

Vamos ilustrar isso com um exemplo. Digamos que o JavaScript seja o protagonista de um livro de fantasia épica. Apenas um garoto de fazenda comum.

Agora, digamos que esse garoto da fazenda encontrou duas armaduras especiais que deram a eles poderes além dos seus.

Quando eles usaram a armadura do navegador, eles ganharam acesso a um determinado conjunto de recursos.

Quando eles usaram a armadura do servidor, eles ganharam acesso a outro conjunto de recursos.

Esses trajes têm alguma sobreposição, porque seus criadores tinham as mesmas necessidades em certos lugares, mas não em outros.

Isso é o que é um ambiente. Um lugar onde o código é executado, onde existem ferramentas que são construídas sobre a linguagem JavaScript existente. Eles não fazem parte da linguagem, mas a linha costuma ser confusa porque usamos essas ferramentas todos os dias quando escrevemos o código.

setTimeout, buscar, e DOM são todos exemplos de APIs da Web. (Você pode veja a lista completa de APIs da Web aqui.) São ferramentas incorporadas ao navegador e disponibilizadas para nós quando o nosso código é executado.

E como sempre rodamos JavaScript em um ambiente, parece que faz parte da linguagem. Mas eles não são.

Portanto, se você já se perguntou por que pode usar fetch em JavaScript ao executá-lo no navegador (mas precisa instalar um pacote ao executá-lo em NodeJS), esse é o motivo. Alguém achou que fetch era uma boa ideia e o construiu como uma ferramenta para o ambiente NodeJS.

Está confuso? Sim!

Mas agora podemos finalmente entender o que acontece com o trabalho do JavaScript e como ele é contratado.

Acontece que é o ambiente que assume o trabalho, e a maneira de fazer com que o ambiente faça esse trabalho é usar funcionalidades que pertencem ao ambiente. Por exemplo buscar ou setTimeout no ambiente do navegador.

O que acontece com o trabalho?

Ótimo. Portanto, o ambiente assume o trabalho. Então o que?

Em algum momento, você precisará obter os resultados de volta. Mas vamos pensar em como isso funcionaria.

Vamos voltar ao exemplo de malabarismo desde o início. Imagine que você pediu uma nova bola e um amigo começou a jogar a bola em você quando você não estava pronto.

Isso seria um desastre. Talvez você pudesse ter sorte e pegá-lo e colocá-lo em sua rotina de forma eficaz. Mas há uma grande chance de que isso faça com que você deixe cair todas as bolas e estrague sua rotina. Não seria melhor se você desse instruções estritas sobre quando receber a bola?

Acontece que existem regras estritas em torno de quando o JavaScript pode receber trabalho delegado.

Essas regras são regidas pelo loop de eventos e envolvem a fila de microtarefas e macrotarefas. Sim eu conheço. Isso é muito. Mas tenha paciência comigo.

autodraw 31 08 2020

Tudo bem. Portanto, quando delegamos código assíncrono ao navegador, o navegador pega e executa o código e assume essa carga de trabalho. Mas pode haver várias tarefas atribuídas ao navegador, portanto, precisamos ter certeza de que podemos priorizar essas tarefas.

É aqui que a fila de microtarefa e a fila de macrotarefa entram em ação. O navegador pegará o trabalho, fará e, em seguida, colocará o resultado em uma das duas filas com base no tipo de trabalho que recebe.

As promessas, por exemplo, são colocadas na fila de microtarefa e têm uma prioridade mais alta.

Eventos e setTimeout são exemplos de trabalho que são colocados na fila de macrotask e têm uma prioridade mais baixa.

Agora, uma vez que o trabalho é concluído e colocado em uma das duas filas, o loop de eventos será executado para frente e para trás e verificará se o JavaScript está ou não pronto para receber os resultados.

Somente quando o JavaScript terminar de executar todo o seu código síncrono e estiver bom e pronto, o loop de eventos começará a ser selecionado nas filas e a devolver as funções ao JavaScript para execução.

Então, vamos dar uma olhada em um exemplo:

setTimeout(() => console.log("hello"), 0) fetch("https://someapi/data").then(response => response.json())                             .then(data => console.log(data))console.log("What soup?")

Qual será o pedido aqui?

  1. Em primeiro lugar, setTimeout é delegado ao navegador, que faz o trabalho e coloca a função resultante na fila de macrotask.
  2. Em segundo lugar, a busca é delegada ao navegador, que faz o trabalho. Ele recupera os dados do terminal e coloca as funções resultantes na fila de microtarefa.
  3. Javascript desconecta “Que sopa”?
  4. O loop de eventos verifica se o JavaScript está pronto ou não para receber os resultados do trabalho enfileirado.
  5. Quando o console.log é concluído, o JavaScript está pronto. O loop de evento seleciona funções enfileiradas da fila de microtarefa, que tem uma prioridade mais alta, e as devolve ao JavaScript para execução.
  6. Depois que a fila de microtarefa está vazia, o retorno de chamada setTimeout é retirado da fila de macrotarefa e devolvido ao JavaScript para execução.

In console:// What soup?// the data from the api// hello

Promessas

Agora você deve ter um bom conhecimento sobre como o código assíncrono é tratado pelo JavaScript e pelo ambiente do navegador. Então, vamos falar sobre promessas.

Uma promessa é uma construção JavaScript que representa um valor futuro desconhecido. Conceitualmente, uma promessa é apenas o JavaScript prometendo retornar um valor. Pode ser o resultado de uma chamada de API ou um objeto de erro de uma solicitação de rede com falha. Você tem a garantia de obter algo.

const promise = new Promise((resolve, reject) => {	// Make a network request   if (response.status === 200) {      resolve(response.body)   } else {      const error = { ... }      reject(error)   }})promise.then(res => {	console.log(res)}).catch(err => {	console.log(err)})

Uma promessa pode ter os seguintes estados:

  • cumprida – ação concluída com sucesso
  • rejeitado – ação falhou
  • pendente – nenhuma ação foi concluída
  • liquidado – foi cumprido ou rejeitado

Uma promessa recebe uma função de resolução e rejeição que pode ser chamada para acionar um desses estados.

Um dos grandes argumentos de venda das promessas é que podemos encadear funções que queremos que aconteçam em caso de sucesso (resolução) ou falha (rejeição):

  • Para registrar uma função para ser executada com sucesso, usamos. Então
  • Para registrar uma função para executar em caso de falha, usamos .catch

// Fetch returns a promisefetch("https://swapi.dev/api/people/1")	.then((res) => console.log("This function is run when the request succeeds", res)    .catch(err => console.log("This function is run when the request fails", err)           // Chaining multiple functions fetch("https://swapi.dev/api/people/1")	.then((res) => doSomethingWithResult(res))    .then((finalResult) => console.log(finalResult))    .catch((err => doSomethingWithErr(err))

Perfeito. Agora vamos dar uma olhada mais de perto em como isso se parece sob o capô, usando fetch como exemplo:

const fetch = (url, options) => {  // simplified  return new Promise((resolve, reject) => {  const xhr = new XMLHttpRequest()  // ... make request  xhr.onload = () => {    const options = {        status: xhr.status,        statusText: xhr.statusText        ...    }        resolve(new Response(xhr.response, options))  }    xhr.onerror = () => {    reject(new TypeError("Request failed"))  }}  fetch("https://swapi.dev/api/people/1")   // Register handleResponse to run when promise resolves	.then(handleResponse)  .catch(handleError)   // conceptually, the promise looks like this now: // { status: "pending", onsuccess: [handleResponse], onfailure: [handleError] }   const handleResponse = (response) => {  // handleResponse will automatically receive the response, ¨  // because the promise resolves with a value and automatically injects into the function   console.log(response) }   const handleError = (response) => {  // handleError will automatically receive the error, ¨  // because the promise resolves with a value and automatically injects into the function   console.log(response) }  // the promise will either resolve or reject causing it to run all of the registered functions in the respective arrays// injecting the value. Let's inspect the happy path:  // 1. XHR event listener fires// 2. If the request was successfull, the onload event listener triggers// 3. The onload fires the resolve(VALUE) function with given value// 4. Resolve triggers and schedules the functions registered with .then    

Portanto, podemos usar promessas para fazer trabalho assíncrono e ter certeza de que podemos lidar com qualquer resultado dessas promessas. Essa é a proposta de valor. Se você quiser saber mais sobre as promessas, pode ler mais sobre elas aqui e aqui.

Quando usamos promessas, encadeamos nossas funções na promessa de lidar com os diferentes cenários.

Isso funciona, mas ainda precisamos lidar com nossa lógica dentro de callbacks (funções aninhadas), uma vez que recebemos nossos resultados de volta. E se pudéssemos usar promessas, mas escrever um código de aparência síncrona? Acontece que podemos.

Async / Await

Async / Await é uma forma de escrever promessas que nos permite escrever código assíncrono de forma síncrona. Vamos dar uma olhada.

const getData = async () => {    const response = await fetch("https://jsonplaceholder.typicode.com/todos/1")    const data = await response.json()        console.log(data)}getData()

Nada mudou sob o capô aqui. Ainda estamos usando promessas para buscar dados, mas agora parece síncrono e não temos mais os blocos .then e .catch.

Async / Await é, na verdade, apenas um açúcar sintático, fornecendo uma maneira de criar código mais fácil de raciocinar, sem alterar a dinâmica subjacente.

Vamos dar uma olhada em como funciona.

Async / Await nos permite usar geradores para pausa a execução de uma função. Quando estamos usando async / await não estamos bloqueando porque a função está devolvendo o controle ao programa principal.

Então, quando a promessa é resolvida, estamos usando o gerador para devolver o controle à função assíncrona com o valor da promessa resolvida.

Você pode ler mais aqui para uma ótima visão geral de geradores e código assíncrono.

Na verdade, agora podemos escrever código assíncrono que se parece com código síncrono. O que significa que é mais fácil raciocinar e podemos usar ferramentas síncronas para tratamento de erros, como try / catch:

const getData = async () => {    try {    	const response = await fetch("https://jsonplaceholder.typicode.com/todos/1")    	const data = await response.json()        console.log(data)    } catch (err) {       console.log(err)    }    }getData()

Tudo bem. Então, como o usamos? Para usar async / await, precisamos preceder a função com async. Isso não a torna uma função assíncrona, apenas nos permite usar o await dentro dela.

Deixar de fornecer a palavra-chave async resultará em um erro de sintaxe ao tentar usar o await dentro de uma função regular.

const getData = async () => {	console.log("We can use await in this function")}

Por causa disso, não podemos usar async / await no código de nível superior. Mas async e await ainda são apenas açúcar sintático em vez de promessas. Portanto, podemos lidar com casos de nível superior com encadeamento de promessa:

async function getData() {  let response = await fetch('http://apiurl.com');}// getData is a promisegetData().then(res => console.log(res)).catch(err => console.log(err); 

Isso expõe outro fato interessante sobre async / await. Ao definir uma função como assíncrona, sempre retornará uma promessa.

Usar async / await pode parecer mágica à primeira vista. Mas, como qualquer mágica, é apenas uma tecnologia suficientemente avançada que evoluiu ao longo dos anos. Esperamos que agora você tenha um conhecimento sólido dos fundamentos e possa usar async / await com confiança.

Se você veio aqui, parabéns. Você acabou de adicionar à sua caixa de ferramentas um conhecimento importante sobre JavaScript e como ele funciona com seus ambientes.

Este é definitivamente um assunto confuso e as linhas nem sempre são claras. Mas agora você espera ter uma compreensão de como o JavaScript funciona com código assíncrono no navegador e um domínio mais forte sobre promessas e assíncrono / aguardar.

Se você gostou deste artigo, também pode gostar do meu canal do Youtube. Atualmente tenho um série de fundamentos da web indo por onde eu passo HTTP, construindo servidores web do zero e mais.

Também há uma série acontecendo construir um aplicativo inteiro com React, se essa for a sua geléia. E pretendo adicionar muito mais conteúdo aqui no futuro, aprofundando os tópicos de JavaScript.

E se você quiser dizer oi ou bater um papo sobre desenvolvimento web, você pode sempre entrar em contato comigo no twitter em @foseberg. Obrigado por ler!