Funções assíncronas permitem escrever código baseado em promessas como se fosse síncrono.
Funções assíncronas são ativadas por padrão no Chrome, Edge, Firefox e Safari, e elas são maravilhosas. Elas permitem escrever código baseado em promessa como se fosse síncrono, mas sem bloquear a linha de execução principal. Elas tornam seu código assíncrono menos "inteligente" e mais legível.
Funções assíncronas funcionam assim:
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
Se você usar a palavra-chave async
antes de uma definição de função, poderá usar
await
na função. Quando você aplica await
a uma promessa, a função é pausada
sem bloqueio até que a promessa seja resolvida. Se a promessa for atendida, você vai receber o valor de volta. Se a promessa for rejeitada, o valor rejeitado será gerado.
Suporte ao navegador
Exemplo: registrar uma busca
Digamos que você queira buscar um URL e registrar a resposta como texto. Veja como fica usando promessas:
function logFetch(url) {
return fetch(url)
.then((response) => response.text())
.then((text) => {
console.log(text);
})
.catch((err) => {
console.error('fetch failed', err);
});
}
E aqui está a mesma coisa usando funções assíncronas:
async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
} catch (err) {
console.log('fetch failed', err);
}
}
É o mesmo número de linhas, mas todos os callbacks sumiram. Isso facilita muito a leitura, especialmente para pessoas menos familiarizadas com promessas.
Valores de retorno assíncronos
Funções assíncronas sempre retornam uma promessa, quer você use await
ou não. Essa
promessa é resolvida com o que a função assíncrona retorna ou é rejeitada com
qualquer coisa que a função assíncrona gera. Então, com:
// wait ms milliseconds
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
... chamar hello()
retorna uma promessa que se cumpre com "world"
.
async function foo() {
await wait(500);
throw Error('bar');
}
... chamar foo()
retorna uma promessa que é rejeitada com Error('bar')
.
Exemplo: streaming de uma resposta
A vantagem das funções assíncronas aumenta em exemplos mais complexos. Digamos que você queira transmitir uma resposta ao registrar as partes e retornar o tamanho final.
Aqui está com promessas:
function getResponseSize(url) {
return fetch(url).then((response) => {
const reader = response.body.getReader();
let total = 0;
return reader.read().then(function processResult(result) {
if (result.done) return total;
const value = result.value;
total += value.length;
console.log('Received chunk', value);
return reader.read().then(processResult);
});
});
}
Confira-me, Jake "portador de promessas" Archibald. Vê como estou chamando
processResult()
dentro dele mesmo para configurar um loop assíncrono? Escrever isso me fez
sentir muito inteligente. Mas, como acontece com a maioria dos códigos "inteligentes", é preciso analisá-lo por algum tempo para descobrir o que ele faz, como uma daquelas imagens de olho mágico dos anos 90.
Vamos tentar novamente com funções assíncronas:
async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;
while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}
return total;
}
Todo o "inteligente" desapareceu. O loop assíncrono que me fez sentir tão presunçoso foi
substituído por um "while-loop" confiável e entediante. Muito melhor. No futuro, você terá iteradores assíncronos, que substituirão o loop while
por um loop "for-of", tornando-o ainda mais elegante.
Outra sintaxe de função assíncrona
Já mostrei async function() {}
, mas a palavra-chave async
pode ser usada com outra sintaxe de função:
Funções de seta
// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
Métodos do objeto
const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(`/avatars/${name}.jpg`);
}
};
storage.getAvatar('jaffathecake').then(…);
Métodos da classe
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);
Tenha cuidado! Evite ser sequencial demais
Mesmo que você esteja escrevendo um código que parece síncrono, não perca a oportunidade de fazer coisas em paralelo.
async function series() {
await wait(500); // Wait 500ms…
await wait(500); // …then wait another 500ms.
return 'done!';
}
O processo acima leva 1.000 ms para ser concluído, enquanto:
async function parallel() {
const wait1 = wait(500); // Start a 500ms timer asynchronously…
const wait2 = wait(500); // …meaning this timer happens in parallel.
await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
return 'done!';
}
O processo acima leva 500 ms para ser concluído, porque as duas esperas acontecem ao mesmo tempo. Vamos conferir um exemplo prático.
Exemplo: como gerar buscas em ordem
Digamos que você queira buscar uma série de URLs e registrá-los o mais rápido possível, na ordem correta.
Respire fundo: eis como isso fica com promessas:
function markHandled(promise) {
promise.catch(() => {});
return promise;
}
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map((url) => {
return markHandled(fetch(url).then((response) => response.text()));
});
// log them in order
return textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise).then((text) => console.log(text));
}, Promise.resolve());
}
Sim, isso mesmo. Estou usando reduce
para encadear uma sequência de promessas. Sou tão
inteligente. Mas essa é uma codificação tão inteligente, que fica melhor sem ela.
No entanto, ao converter o item acima para uma função assíncrona, é tentador ser sequencial demais:
async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } }
function markHandled(...promises) { Promise.allSettled(promises); } async function logInOrder(urls) { // fetch all the URLs in parallel const textPromises = urls.map(async (url) => { const response = await fetch(url); return response.text(); }); markHandled(...textPromises); // log them in sequence for (const textPromise of textPromises) { console.log(await textPromise); } }
Solução alternativa para suporte do navegador: geradores
Se você estiver segmentando navegadores compatíveis com geradores (o que inclui a versão mais recente de todos os principais navegadores), poderá usar funções assíncronas de polyfill.
O Babel vai fazer isso por você. Confira um exemplo do Babel REPL
- observe a semelhança do código transcompilado. Essa transformação faz parte da predefinição do Babel es2017 (link em inglês).
Recomendo a abordagem de transcompilação, porque você pode desativá-la quando os navegadores de destino oferecerem suporte a funções assíncronas, mas se você realmente não quiser usar um transpilador, poderá usar o polyfill do Babel (em inglês) e usá-lo por conta própria. Em vez de:
async function slowEcho(val) {
await wait(1000);
return val;
}
...você incluiria o polyfill e escreveria:
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
Você precisa transmitir um gerador (function*
) para createAsyncFunction
e usar yield
em vez de await
. Fora isso, funciona da mesma forma.
Alternativa: regenerador
Se você estiver segmentando navegadores mais antigos, o Babel também pode transcompilar geradores, permitindo que você use funções assíncronas até o IE8. Para fazer isso, você precisa da predefinição do Babel es2017 e da predefinição es2015.
A saída não é tão bonita, então cuidado com o excesso de código.
Sincronize tudo!
Quando as funções assíncronas estiverem em todos os navegadores, use-as em todas as funções que retornam promessas. Elas não apenas tornam o código mais ordenado, mas também garantem que a função sempre retornará uma promessa.
Fiquei muito empolgado com as funções assíncronas em 2014 e é ótimo vê-las chegar, de verdade, nos navegadores. Opa!