Voltemos ao problema mencionado no capítulo Introdução: callbacks: temos uma sequência de tarefas assíncronas a serem executadas uma após a outra — por exemplo, carregar scripts. Como podemos codificá-lo bem?
As promessas fornecem algumas receitas para fazer isso.
Neste capítulo abordamos o encadeamento de promessas.
Parece assim:
nova Promessa(função(resolver, rejeitar) { setTimeout(() => resolver(1), 1000); // (*) }).then(função(resultado) { // (**) alerta(resultado); //1 resultado de retorno * 2; }).então(função(resultado) { // (***) alerta(resultado); //2 resultado de retorno * 2; }).então(função(resultado) { alerta(resultado); //4 resultado de retorno * 2; });
A ideia é que o resultado seja passado pela cadeia de manipuladores .then
.
Aqui o fluxo é:
A promessa inicial é resolvida em 1 segundo (*)
,
Então o manipulador .then
é chamado (**)
, que por sua vez cria uma nova promessa (resolvida com valor 2
).
O próximo then
(***)
obtém o resultado do anterior, processa (duplica) e passa para o próximo manipulador.
…e assim por diante.
À medida que o resultado é passado ao longo da cadeia de manipuladores, podemos ver uma sequência de chamadas alert
: 1
→ 2
→ 4
.
A coisa toda funciona, porque cada chamada para um .then
retorna uma nova promessa, para que possamos chamar o próximo .then
.
Quando um manipulador retorna um valor, ele se torna o resultado dessa promessa, então o próximo .then
é chamado com ele.
Um erro clássico de novato: tecnicamente também podemos adicionar muitos .then
a uma única promessa. Isso não é encadeamento.
Por exemplo:
deixe promessa = new Promise(function(resolver, rejeitar) { setTimeout(() => resolver(1), 1000); }); promessa.então(função(resultado) { alerta(resultado); //1 resultado de retorno * 2; }); promessa.então(função(resultado) { alerta(resultado); //1 resultado de retorno * 2; }); promessa.então(função(resultado) { alerta(resultado); //1 resultado de retorno * 2; });
O que fizemos aqui foi apenas adicionar vários manipuladores a uma promessa. Eles não passam o resultado um para o outro; em vez disso, eles o processam de forma independente.
Aqui está a imagem (compare com o encadeamento acima):
Todos .then
com a mesma promessa, obtêm o mesmo resultado – o resultado dessa promessa. Portanto, no código acima, todos alert
mostram o mesmo: 1
.
Na prática, raramente precisamos de vários manipuladores para uma promessa. O encadeamento é usado com muito mais frequência.
Um manipulador usado em .then(handler)
pode criar e retornar uma promessa.
Nesse caso, outros manipuladores esperam até que ele se acalme e então obtenham o resultado.
Por exemplo:
nova Promessa(função(resolver, rejeitar) { setTimeout(() => resolver(1), 1000); }).então(função(resultado) { alerta(resultado); //1 retornar nova promessa((resolver, rejeitar) => { // (*) setTimeout(() => resolver(resultado * 2), 1000); }); }).então(função(resultado) { // (**) alerta(resultado); //2 retornar nova Promessa((resolver, rejeitar) => { setTimeout(() => resolver(resultado * 2), 1000); }); }).então(função(resultado) { alerta(resultado); //4 });
Aqui o primeiro .then
mostra 1
e retorna new Promise(…)
na linha (*)
. Depois de um segundo ele é resolvido e o resultado (o argumento de resolve
, aqui é result * 2
) é passado para o manipulador do segundo .then
. Esse manipulador está na linha (**)
, mostra 2
e faz a mesma coisa.
Portanto, a saída é a mesma do exemplo anterior: 1 → 2 → 4, mas agora com atraso de 1 segundo entre as chamadas alert
.
O retorno de promessas nos permite construir cadeias de ações assíncronas.
Vamos usar esse recurso com o prometido loadScript
, definido no capítulo anterior, para carregar scripts um por um, em sequência:
loadScript("https://javascript.info/article/promise-chaining/one.js") .então(função(script) { return loadScript("https://javascript.info/article/promise-chaining/two.js"); }) .então(função(script) { return loadScript("https://javascript.info/article/promise-chaining/três.js"); }) .então(função(script) { // usa funções declaradas em scripts // para mostrar que eles realmente carregaram um(); dois(); três(); });
Este código pode ser um pouco mais curto com funções de seta:
loadScript("https://javascript.info/article/promise-chaining/one.js") .then(script => loadScript("https://javascript.info/article/promise-chaining/two.js")) .then(script => loadScript("https://javascript.info/article/promise-chaining/três.js")) .então(script => { // os scripts são carregados, podemos usar funções declaradas lá um(); dois(); três(); });
Aqui, cada chamada loadScript
retorna uma promessa e a próxima .then
é executada quando é resolvida. Em seguida, inicia o carregamento do próximo script. Portanto, os scripts são carregados um após o outro.
Podemos adicionar mais ações assíncronas à cadeia. Observe que o código ainda está “plano” – ele cresce para baixo, não para a direita. Não há sinais da “pirâmide da destruição”.
Tecnicamente, poderíamos adicionar .then
diretamente a cada loadScript
, assim:
loadScript("https://javascript.info/article/promise-chaining/one.js").then(script1 => { loadScript("https://javascript.info/article/promise-chaining/two.js").then(script2 => { loadScript("https://javascript.info/article/promise-chaining/três.js").then(script3 => { // esta função tem acesso às variáveis script1, script2 e script3 um(); dois(); três(); }); }); });
Este código faz o mesmo: carrega 3 scripts em sequência. Mas “cresce para a direita”. Portanto, temos o mesmo problema dos retornos de chamada.
As pessoas que começam a usar promessas às vezes não sabem sobre encadeamento, então escrevem desta forma. Geralmente, o encadeamento é preferido.
Às vezes não há problema em escrever .then
diretamente, porque a função aninhada tem acesso ao escopo externo. No exemplo acima, o retorno de chamada mais aninhado tem acesso a todas as variáveis script1
, script2
, script3
. Mas isso é uma exceção e não uma regra.
Entãoables
Para ser mais preciso, um manipulador pode retornar não exatamente uma promessa, mas um objeto chamado “thenable” – um objeto arbitrário que possui um método .then
. Será tratado da mesma forma que uma promessa.
A ideia é que bibliotecas de terceiros possam implementar seus próprios objetos “compatíveis com promessas”. Eles podem ter um conjunto estendido de métodos, mas também ser compatíveis com promessas nativas, porque implementam .then
.
Aqui está um exemplo de um objeto thenable:
classe Entãoable { construtor(num) { isto.num = num; } then(resolver, rejeitar) { alerta(resolver); //função() {código nativo} // resolve com this.num*2 após 1 segundo setTimeout(() => resolve(this.num * 2), 1000); // (**) } } nova promessa(resolver => resolver(1)) .então(resultado => { retornar novo Thenable(resultado); // (*) }) .então(alerta); // mostra 2 após 1000ms
JavaScript verifica o objeto retornado pelo manipulador .then
na linha (*)
: se ele tiver um método que pode ser chamado chamado then
, então ele chama esse método fornecendo funções nativas resolve
, reject
como argumentos (semelhante a um executor) e espera até que um deles é chamado. No exemplo acima resolve(2)
é chamado após 1 segundo (**)
. Em seguida, o resultado é passado mais adiante na cadeia.
Esse recurso nos permite integrar objetos personalizados com cadeias de promessas sem precisar herdar de Promise
.
Na programação frontend, as promessas são frequentemente usadas para solicitações de rede. Então, vamos ver um exemplo extenso disso.
Usaremos o método fetch para carregar as informações sobre o usuário do servidor remoto. Possui muitos parâmetros opcionais abordados em capítulos separados, mas a sintaxe básica é bastante simples:
deixe promessa = buscar(url);
Isso faz uma solicitação de rede ao url
e retorna uma promessa. A promessa é resolvida com um objeto response
quando o servidor remoto responde com cabeçalhos, mas antes que a resposta completa seja baixada .
Para ler a resposta completa, devemos chamar o método response.text()
: ele retorna uma promessa que é resolvida quando o texto completo é baixado do servidor remoto, tendo esse texto como resultado.
O código abaixo faz uma solicitação ao user.json
e carrega seu texto do servidor:
buscar('https://javascript.info/article/promise-chaining/user.json') // .then abaixo é executado quando o servidor remoto responde .então(função(resposta) { //response.text() retorna uma nova promessa que é resolvida com o texto de resposta completo // quando carrega retornar resposta.text(); }) .então(função(texto) { // ...e aqui está o conteúdo do arquivo remoto alerta(texto); // {"nome": "iliakan", "isAdmin": verdadeiro} });
O objeto response
retornado da fetch
também inclui o método response.json()
que lê os dados remotos e os analisa como JSON. No nosso caso, isso é ainda mais conveniente, então vamos mudar para isso.
Também usaremos funções de seta por questões de brevidade:
// igual ao acima, mas response.json() analisa o conteúdo remoto como JSON buscar('https://javascript.info/article/promise-chaining/user.json') .then(resposta => resposta.json()) .then(usuário => alerta(nome do usuário)); // iliakan, obteve o nome de usuário
Agora vamos fazer algo com o usuário carregado.
Por exemplo, podemos fazer mais uma solicitação ao GitHub, carregar o perfil do usuário e mostrar o avatar:
// Faça uma solicitação para user.json buscar('https://javascript.info/article/promise-chaining/user.json') //Carrega como json .then(resposta => resposta.json()) // Faça uma solicitação ao GitHub .then(usuário => fetch(`https://api.github.com/users/${user.name}`)) //Carrega a resposta como json .then(resposta => resposta.json()) // Mostra a imagem do avatar (githubUser.avatar_url) por 3 segundos (talvez anime-a) .then(githubUser => { deixe img = document.createElement('img'); img.src=githubUser.avatar_url; img.className = "exemplo-avatar-promessa"; documento.body.append(img); setTimeout(() => img.remove(), 3000); // (*) });
O código funciona; veja comentários sobre os detalhes. Porém, há um problema potencial nisso, um erro típico de quem começa a usar promessas.
Veja a linha (*)
: como podemos fazer algo depois que o avatar terminar de aparecer e for removido? Por exemplo, gostaríamos de mostrar um formulário para editar esse usuário ou outra coisa. A partir de agora, não há como.
Para tornar a cadeia extensível, precisamos retornar uma promessa que será resolvida quando o avatar terminar de ser exibido.
Assim:
buscar('https://javascript.info/article/promise-chaining/user.json') .then(resposta => resposta.json()) .then(usuário => fetch(`https://api.github.com/users/${user.name}`)) .then(resposta => resposta.json()) .then(githubUser => new Promise(function(resolver, rejeitar) { // (*) deixe img = document.createElement('img'); img.src=githubUser.avatar_url; img.className = "exemplo-avatar-promessa"; documento.body.append(img); setTimeout(() => { img.remove(); resolver(githubUser); // (**) }, 3.000); })) // dispara após 3 segundos .then(githubUser => alert(`Terminado mostrando ${githubUser.name}`));
Ou seja, o manipulador .then
na linha (*)
agora retorna new Promise
, que é resolvido somente após a chamada de resolve(githubUser)
em setTimeout
(**)
. O próximo .then
da cadeia esperará por isso.
Como boa prática, uma ação assíncrona deve sempre retornar uma promessa. Isso permite planejar ações a partir daí; mesmo que não planejemos estender a cadeia agora, poderemos precisar dela mais tarde.
Finalmente, podemos dividir o código em funções reutilizáveis:
função carregarJson(url) { retornar buscar (url) .then(resposta => resposta.json()); } função carregarGithubUser(nome) { retornar loadJson(`https://api.github.com/users/${nome}`); } function mostrarAvatar(githubUser) { retornar nova Promessa(função(resolver, rejeitar) { deixe img = document.createElement('img'); img.src=githubUser.avatar_url; img.className = "exemplo-avatar-promessa"; documento.body.append(img); setTimeout(() => { img.remove(); resolver(githubUser); }, 3.000); }); } // Use-os: loadJson('https://javascript.info/article/promise-chaining/user.json') .then(usuário => carregarGithubUser(usuário.nome)) .então(mostrar Avatar) .then(githubUser => alert(`Terminado mostrando ${githubUser.name}`)); // ...
Se um manipulador .then
(ou catch/finally
, não importa) retornar uma promessa, o resto da cadeia esperará até que ela seja resolvida. Quando isso acontece, seu resultado (ou erro) é transmitido posteriormente.
Aqui está uma imagem completa:
Esses fragmentos de código são iguais? Em outras palavras, eles se comportam da mesma maneira em qualquer circunstância, para qualquer função de manipulador?
promessa.então(f1).catch(f2);
Contra:
promessa.então(f1, f2);
A resposta curta é: não, eles não são iguais :
A diferença é que se ocorrer um erro em f1
, ele será tratado por .catch
aqui:
promessa .então(f1) .catch(f2);
…Mas não aqui:
promessa .então(f1, f2);
Isso ocorre porque um erro é transmitido pela cadeia e na segunda parte do código não há cadeia abaixo de f1
.
Em outras palavras, .then
passa resultados/erros para o próximo .then/catch
. Portanto, no primeiro exemplo, há um catch
abaixo, e no segundo não, então o erro não é tratado.