Design assíncrono / aguardado do JavaScript: pontos fortes, armadilhas e padrões de uso

A construção assíncrona / aguardada apareceu no padrão ES7. Pode ser considerada uma melhoria notável no campo da programação assíncrona em JavaScript. Ele permite que você escreva um código que pareça síncrono, mas é usado para resolver tarefas assíncronas e não bloqueia o encadeamento principal. Apesar do fato de que assíncrono / espera é um ótimo recurso novo do idioma, usá-lo corretamente não é tão simples. O material, cuja tradução publicamos hoje, é dedicado a um estudo abrangente de assíncrono / espera e uma história sobre como usar esse mecanismo de maneira correta e eficaz.

imagem

Pontos fortes de assíncrono / aguardar


O benefício mais importante que um programador usando a construção assíncrona / espera obtém é que torna possível escrever código assíncrono em um estilo específico para o código síncrono. Compare o código escrito usando async / waitit com o código baseado em promessas.

// async/await async getBooksByAuthorWithAwait(authorId) {  const books = await bookModel.fetchAll();  return books.filter(b => b.authorId === authorId); } //  getBooksByAuthorWithPromise(authorId) {  return bookModel.fetchAll()    .then(books => books.filter(b => b.authorId === authorId)); } 

É fácil perceber que a versão assíncrona / aguardada do exemplo é mais compreensível do que sua versão, na qual a promessa é usada. Se você não prestar atenção à palavra-chave await , esse código será semelhante a um conjunto regular de instruções executadas de forma síncrona - como no JavaScript familiar ou em qualquer outra linguagem síncrona como Python.

A atratividade de async / waitit não se deve apenas à maior legibilidade do código. Além disso, esse mecanismo possui excelente suporte ao navegador, o que não requer nenhuma solução alternativa. Portanto, hoje as funções assíncronas são totalmente compatíveis com todos os principais navegadores.


Todos os principais navegadores suportam funções assíncronas ( caniuse.com )

Esse nível de suporte significa, por exemplo, que o código usando async / waitit não precisa ser transposto . Além disso, facilita a depuração, o que talvez seja ainda mais importante do que a falta de necessidade de transpilação.

A figura a seguir mostra o processo de depuração de uma função assíncrona. Aqui, ao definir um ponto de interrupção na primeira instrução da função e ao executar o comando Step Over, quando o depurador atingir a linha em que a palavra-chave await é usada, você poderá observar como o depurador pausa por um tempo, aguardando o bookModel.fetchAll() função bookModel.fetchAll() e, em seguida, pula para a linha em que o comando .filter() é .filter() ! Esse processo de depuração parece muito mais simples do que as promessas de depuração. Aqui, ao depurar código semelhante, você teria que definir outro ponto de interrupção na linha .filter() .


Depurando uma função assíncrona. O depurador aguardará a conclusão da linha de espera e passará para a próxima linha após a conclusão da operação

Outro ponto forte do mecanismo em consideração, que é menos óbvio do que o que já examinamos, é a presença da palavra async chave async aqui. No nosso caso, seu uso garante que o valor retornado por getBooksByAuthorWithAwait() seja uma promessa. Como resultado, você pode usar com segurança a construção getBooksByAuthorWithAwait().then(...) ou await getBooksByAuthorWithAwait() construção await getBooksByAuthorWithAwait() no código que chama essa função. Considere o seguinte exemplo (observe que isso não é recomendado):

 getBooksByAuthorWithPromise(authorId) { if (!authorId) {   return null; } return bookModel.fetchAll()   .then(books => books.filter(b => b.authorId === authorId)); } } 

Aqui, a função getBooksByAuthorWithPromise() pode, se estiver tudo bem, retornar uma promessa ou, se algo der errado - null . Como resultado, se ocorrer um erro, você não poderá chamar .then() com segurança .then() . Ao declarar funções usando a async erros desse tipo são impossíveis.

Sobre a percepção errônea de assíncrono / aguardar


Em algumas publicações, a construção assíncrona / espera é comparada às promessas e diz-se que representa a próxima geração da evolução da programação JavaScript assíncrona. Com isso, com todo o respeito devido aos autores de tais publicações, permito-me discordar. O assíncrono / espera é uma melhoria, mas não passa de "açúcar sintático", cuja aparência não leva a uma mudança completa no estilo de programação.

Em essência, funções assíncronas são promessas. Antes de um programador poder usar adequadamente a construção assíncrona / aguardada, ele deve estudar bem as promessas. Além disso, na maioria dos casos, trabalhando com funções assíncronas, você precisa usar promessas.

Dê uma olhada nas getBooksByAuthorWithAwait() e getBooksByAuthorWithPromises() do exemplo acima. Observe que eles são idênticos não apenas em termos de funcionalidade. Eles também têm exatamente as mesmas interfaces.

Tudo isso significa que se você chamar diretamente a função getBooksByAuthorWithAwait() , ela retornará a promessa.

De fato, a essência do problema que estamos falando aqui é a percepção incorreta do novo design, quando cria uma sensação enganosa de que uma função síncrona pode ser convertida em assíncrona devido ao uso simples do async e await palavras-chave e não pensar em mais nada.

Armadilhas do assíncrono / aguardam


Vamos falar sobre os erros mais comuns que podem ser cometidos usando async / waitit. Em particular, sobre o uso irracional de chamadas sucessivas de funções assíncronas.

Embora a palavra-chave await possa fazer com que o código pareça síncrono, use-o, vale lembrar que o código é assíncrono, o que significa que você precisa ter muito cuidado com a chamada seqüencial de funções assíncronas.

 async getBooksAndAuthor(authorId) { const books = await bookModel.fetchAll(); const author = await authorModel.fetch(authorId); return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

Esse código, em termos de lógica, parece correto. No entanto, há um problema sério. É assim que funciona.

  1. As chamadas do sistema await bookModel.fetchAll() e aguardam a .fetchAll() comando .fetchAll() .
  2. Após receber o resultado de bookModel.fetchAll() await authorModel.fetch(authorId) será chamado.

Observe que a chamada para authorModel.fetch(authorId) é independente dos resultados da chamada para bookModel.fetchAll() e, de fato, esses dois comandos podem ser executados em paralelo. No entanto, o uso de await resulta nessas duas chamadas sendo executadas seqüencialmente. O tempo total de execução seqüencial desses dois comandos será maior que o tempo de execução paralela.

Aqui está a abordagem correta para escrever esse código:

 async getBooksAndAuthor(authorId) { const bookPromise = bookModel.fetchAll(); const authorPromise = authorModel.fetch(authorId); const book = await bookPromise; const author = await authorPromise; return {   author,   books: books.filter(book => book.authorId === authorId), }; } 

Considere outro exemplo do uso indevido de funções assíncronas. Isso ainda é pior do que no exemplo anterior. Como você pode ver, para carregar de forma assíncrona uma lista de certos elementos, precisamos confiar nas possibilidades de promessas.

 async getAuthors(authorIds) { //  ,     // const authors = _.map( //   authorIds, //   id => await authorModel.fetch(id)); //   const promises = _.map(authorIds, id => authorModel.fetch(id)); const authors = await Promise.all(promises); } 

Em poucas palavras, para usar corretamente funções assíncronas, você precisa, como no momento em que isso não era possível, primeiro pense em operações assíncronas e depois escreva o código usando await . Em casos complexos, provavelmente será mais fácil usar promessas diretamente.

Tratamento de erros


Ao usar promessas, a execução do código assíncrono pode terminar conforme o esperado - eles dizem que a promessa foi resolvida com sucesso ou com um erro - e dizem que a promessa foi rejeitada. Isso nos permite usar .then() e .catch() , respectivamente. No entanto, o tratamento de erros usando o mecanismo assíncrono / espera pode ser complicado.

▍ construção try / catch


A maneira padrão de lidar com erros ao usar async / waitit é com a construção try / catch. Eu recomendo usar essa abordagem. Ao fazer uma chamada em espera, o valor retornado quando a promessa é rejeitada é apresentado como uma exceção. Aqui está um exemplo:

 class BookModel { fetchAll() {   return new Promise((resolve, reject) => {     window.setTimeout(() => { reject({'error': 400}) }, 1000);   }); } } // async/await async getBooksByAuthorWithAwait(authorId) { try { const books = await bookModel.fetchAll(); } catch (error) { console.log(error);    // { "error": 400 } } 

O erro detectado no catch é exatamente o valor obtido quando a promessa é rejeitada. Depois de capturar uma exceção, podemos aplicar várias abordagens para trabalhar com ela:

  • Você pode manipular a exceção e retornar o valor normal. Se você não usar a expressão de return no catch para retornar o que é esperado após a execução da função assíncrona, isso será equivalente ao uso do comando return undefined ;
  • Você pode simplesmente passar o erro para o local onde o código que falhou foi chamado e permitir que ele seja processado lá. Você pode gerar um erro diretamente usando um comando como throw error; , que permite usar a função async getBooksByAuthorWithAwait() na cadeia de promessas. Ou seja, ele pode ser chamado usando a construção getBooksByAuthorWithAwait().then(...).catch(error => ...) . Além disso, você pode agrupar o erro em um objeto Error , que pode parecer throw new Error(error) . Isso permitirá, por exemplo, ao enviar informações de erro para o console, exibir a pilha de chamadas completa.
  • O erro pode ser representado como uma promessa rejeitada, parece return Promise.reject(error) . Nesse caso, isso é equivalente ao comando throw error , não sendo recomendado.

Aqui estão os benefícios do uso da construção try / catch:

  • Tais ferramentas de tratamento de erros existem na programação há muito tempo, são simples e compreensíveis. Digamos, se você tiver experiência em programação em outras linguagens, como C ++ ou Java, entenderá facilmente o dispositivo try / catch em JavaScript.
  • Você pode fazer várias chamadas em espera em um bloco try / catch, o que permite lidar com todos os erros em um só lugar, se você não precisar lidar separadamente com erros em cada etapa da execução do código.

Note-se que há uma desvantagem no mecanismo try / catch. Como try / catch captura todas as exceções que ocorrem no bloco try , essas exceções não relacionadas a promessas também serão inseridas no manipulador de catch . Dê uma olhada neste exemplo.

 class BookModel { fetchAll() {   cb();    //    ,   `cb`  ,       return fetch('/books'); } } try { bookModel.fetchAll(); } catch(error) { console.log(error);  //       "cb is not defined" } 

Se você executar esse código, verá a mensagem de erro ReferenceError: cb is not defined no console. Esta mensagem é emitida pelo comando console.log() do catch , e não pelo próprio JavaScript. Em alguns casos, esses erros levam a graves consequências. Por exemplo, se chamar bookModel.fetchAll(); Se você estiver oculto em uma série de chamadas de função e uma das chamadas "engolir" um erro, será muito difícil detectar esse erro.

Return Retorno de função de dois valores


A inspiração para a próxima maneira de lidar com erros no código assíncrono é Go. Ele permite que funções assíncronas retornem um erro e um resultado. Leia mais sobre isso aqui .

Em poucas palavras, funções assíncronas, com esta abordagem, podem ser usadas assim:

 [err, user] = await to(UserModel.findById(1)); 

Pessoalmente, não gosto disso, porque esse método de tratamento de erros introduz o estilo de programação Go no JavaScript, que parece não natural, embora, em alguns casos, possa ser muito útil.

▍Utilização de .catch


A maneira final de lidar com os erros, sobre os quais falaremos, é usar .catch() .

Pense em como a await funciona. Ou seja, o uso dessa palavra-chave faz com que o sistema aguarde até que a promessa conclua seu trabalho. Além disso, lembre-se de que um comando no formato promise.catch() também retorna uma promessa. Tudo isso sugere que erros de função assíncrona podem ser manipulados assim:

 // books   undefined   , //    catch     let books = await bookModel.fetchAll() .catch((error) => { console.log(error); }); 

Dois pequenos problemas são característicos dessa abordagem:

  • Essa é uma mistura de promessas e funções assíncronas. Para usar isso, é necessário, como em outros casos semelhantes, entender as características do trabalho das promessas.
  • Essa abordagem não é intuitiva, pois o tratamento de erros é realizado em um local incomum.

Sumário


A construção assíncrona / aguardada, que foi introduzida no ES7, é definitivamente uma melhoria nos mecanismos de programação assíncrona do JavaScript. Isso pode facilitar a leitura e o código de depuração. No entanto, para usar corretamente o assíncrono / aguardar, é necessário um profundo entendimento das promessas, uma vez que o assíncrono / aguardar é apenas "açúcar sintático" baseado em promessas.

Esperamos que este material tenha permitido que você se familiarize com async / wait, e o que você aprendeu aqui o salvará de alguns erros comuns que surgem ao usar essa construção.

Caros leitores! Você usa a construção assíncrona / espera no JavaScript? Nesse caso, informe-nos como você lida com erros no código assíncrono.

Source: https://habr.com/ru/post/pt414373/


All Articles