As promessas simplificam as computações assíncronas e adiadas. Uma promessa representa uma operação que ainda não foi concluída.
Desenvolvedores, preparem-se para um momento crucial na história do desenvolvimento da Web.
[Drumroll begins]
As promessas chegaram ao JavaScript!
[Fireworks explode, glittery paper rains from above, the crowd goes wild]
Nesse ponto, você se enquadra em uma destas categorias:
- As pessoas estão torcendo ao seu redor, mas você não sabe do que se trata tudo isso. Talvez você nem saiba o que é uma "promessa". Você daria de ombros, mas o peso do papel com glitter está pesando nos seus ombros. Se sim, não se preocupe, demorou muito para eu descobrir por que deveria me importar com essas coisas. É melhor começar do início.
- Você dá um soco no ar! Já tava na hora, certo? Você já usou essas coisas de promessa antes, mas incomoda você que todas as implementações têm uma API ligeiramente diferente. Qual é a API para a versão oficial do JavaScript? Comece com a terminologia.
- Você já sabia disso e está zombando de quem está pulando de alegria, como se fosse novidade. Aproveite sua superioridade por um momento e depois acesse a referência da API.
Suporte a navegadores e polyfill
Para trazer os navegadores que não têm uma implementação de promessas completa para o compliance de especificação ou adicionar promessas a outros navegadores e Node.js, confira o polyfill (2k gzipped).
Por que todo esse alvoroço?
O JavaScript tem um único encadeamento, o que significa que dois bits de script não podem ser executados ao mesmo tempo. Eles precisam ser executados um após o outro. Nos navegadores, o JavaScript compartilha uma linha de execução com muitas outras coisas que variam de navegador para navegador. No entanto, normalmente o JavaScript está na mesma fila que a pintura, atualizando estilos e processando ações do usuário, como destacar texto e interagir com controles de formulário. A atividade em uma delas atrasa as outras.
Como um ser humano, você tem vários processos em andamento. Você pode digitar com vários dedos, dirigir e manter uma conversa ao mesmo tempo. A única função de bloqueio com que precisamos lidar é espirrar, em que toda a atividade atual precisa ser suspensa durante o espirro. Isso é muito irritante, principalmente quando você está dirigindo e tentando manter uma conversa. Não desejamos escrever um código que seja lento.
Você provavelmente usou eventos e callbacks para contornar esse problema. Estes são os eventos:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
Isso não é nada fácil. Recebemos a imagem, adicionamos alguns listeners e, em seguida, o JavaScript pode parar de ser executado até que um desses listeners seja chamado.
Infelizmente, no exemplo acima, é possível que os eventos tenham acontecido antes de começarmos a ouvir. Portanto, precisamos contornar esse problema usando a propriedade "complete" das imagens:
var img1 = document.querySelector('.img-1');
function loaded() {
// woo yey image loaded
}
if (img1.complete) {
loaded();
}
else {
img1.addEventListener('load', loaded);
}
img1.addEventListener('error', function() {
// argh everything's broken
});
Isso não captura imagens que geraram erros antes de termos a chance de ouvir elas. Infelizmente, o DOM não oferece uma maneira de fazer isso. Além disso, ele está carregando uma imagem. As coisas ficam ainda mais complexas se quisermos saber quando um conjunto de imagens foi carregado.
Os eventos nem sempre são a melhor opção
Os eventos são ótimos para coisas que podem acontecer várias vezes no mesmo
objeto: keyup
, touchstart
etc. Com esses eventos, não importa muito
o que aconteceu antes de você anexar o listener. Mas, quando se trata de
sucesso/falha assíncrona, o ideal é algo como este:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
Isso é o que as promessas fazem, mas com nomes melhores. Se os elementos de imagem HTML tivessem um método "ready" que retornasse uma promessa, poderíamos fazer o seguinte:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
No nível mais básico, as promessas são um pouco parecidas com os listeners de eventos, exceto:
- Uma promessa só pode ser bem-sucedida ou falhar uma vez. Ele não pode ter sucesso ou falhar duas vezes, nem alternar de sucesso para falha ou vice-versa.
- Se uma promessa tiver sido bem-sucedida ou falhar e você adicionar um callback de sucesso/falha posteriormente, o callback correto será chamado, mesmo que o evento tenha ocorrido antes.
Isso é extremamente útil para sucesso/falha assíncrona, porque você está menos interessado no momento exato em que algo ficou disponível e mais interessado em reagir ao resultado.
Terminologia de promise
Domenic Denicola revisou o primeiro rascunho deste artigo e me deu nota "F" pela terminologia. Ele me colocou de castigo, me obrigou a copiar Estados e destinos 100 vezes e escreveu uma carta preocupada para meus pais. Apesar disso, ainda confundo muita coisa com a terminologia, mas aqui estão os conceitos básicos:
Uma promessa pode ser:
- fulfilled: a ação relacionada à promessa foi concluída.
- rejeitada: a ação relacionada à promessa falhou.
- pendente: ainda não foi cumprida nem recusada.
- Resolved: foi atendida ou recusada.
A especificação
também usa o termo thenable para descrever um objeto semelhante a uma promessa,
que tem um método then
. Esse termo me lembra do ex-técnico da seleção
inglesa de futebol Terry Venables, então
vou usá-lo o mínimo possível.
As promessas chegam ao JavaScript!
As promessas já existem há algum tempo na forma de bibliotecas, como:
As promessas acima e do JavaScript compartilham um comportamento comum e padronizado chamado Promises/A+. Se você for um usuário do jQuery, ele terá algo semelhante chamado Deferreds. No entanto, os adiados não são compatíveis com Promise/A+, o que os torna diferentes e menos úteis, portanto, tenha cuidado. O jQuery também tem um tipo de promessa, mas é apenas um subconjunto de adiados e tem os mesmos problemas.
Embora as implementações de promessa sigam um comportamento padronizado, as APIs gerais são diferentes. As promessas do JavaScript são semelhantes na API do RSVP.js. Veja como criar uma promessa:
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
O construtor da promessa usa um argumento, um callback com dois parâmetros, resolve e rejeita. Faça algo no callback, talvez assíncrono, e chame resolve se tudo funcionar. Caso contrário, chame reject.
Como throw
no JavaScript antigo, é comum, mas não obrigatório, rejeitar com um objeto Error. O benefício dos objetos Error é que eles capturam um
stack trace, tornando as ferramentas de depuração mais úteis.
Confira como usar essa promessa:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then()
recebe dois argumentos: um callback para um caso de sucesso e outro
para o caso de falha. Ambos são opcionais, então você pode adicionar um callback apenas para o
caso de sucesso ou falha.
As promessas do JavaScript começaram no DOM como "Futures", foram renomeadas para "Promises" e, finalmente, movidas para o JavaScript. Ter esses elementos no JavaScript, em vez do DOM, é ótimo porque eles vão estar disponíveis em contextos JS que não são de navegador, como Node.js. Se eles são usados nas APIs principais é outra questão.
Embora sejam um recurso do JavaScript, o DOM não tem medo de usá-los. Na verdade, todas as novas APIs DOM com métodos de sucesso/falha assíncronos vão usar promessas. Isso já está acontecendo com Gerenciamento de cota, Eventos de carregamento de fontes, ServiceWorker, Web MIDI, Streams e muito mais.
Compatibilidade com outras bibliotecas
A API de promessas do JavaScript vai tratar qualquer coisa com um método then()
como
semelhante a uma promessa (ou thenable
no suspiro de promessas). Portanto, se você usar uma biblioteca
que retorna uma promessa Q, tudo bem, ela vai funcionar bem com as novas
promessas do JavaScript.
No entanto, como mencionei, os Deferreds do jQuery são um pouco... inúteis. Felizmente, é possível convertê-las em promessas padrão, o que vale a pena fazer assim que possível:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
Aqui, o $.ajax
do jQuery retorna um Deferred. Como ele tem um método then()
,
Promise.resolve()
pode transformá-lo em uma promessa do JavaScript. No entanto,
às vezes, os adiados transmitem vários argumentos para os callbacks, por exemplo:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
As promessas do JS ignoram todas, exceto a primeira:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
Felizmente, isso geralmente é o que você quer, ou pelo menos dá acesso ao que você quer. Além disso, o jQuery não segue a convenção de transmitir objetos de erro para rejeições.
Código assíncrono complexo ficou mais fácil
Certo, vamos programar algumas coisas. Digamos que queremos:
- Iniciar um ícone de carregamento para indicar o carregamento
- Buscar um JSON para uma história, que fornece o título e os URLs de cada capítulo
- Adicionar título à página
- Buscar cada capítulo
- Adicionar a história à página
- Parar o ícone de carregamento
… mas também informe ao usuário se algo deu errado no caminho. Também vamos querer parar o ícone de carregamento nesse ponto, senão ele vai continuar girando, vai ficar confuso e vai colidir com outra interface.
Obviamente, você não usaria o JavaScript para exibir uma história, a exibição do HTML é mais rápida, mas esse padrão é muito comum ao lidar com APIs: várias buscas de dados, depois faça algo quando tudo estiver pronto.
Para começar, vamos buscar dados da rede:
Como usar o XMLHttpRequest com promessas
As APIs antigas serão atualizadas para usar promessas, se possível de forma compatível
com versões anteriores. XMLHttpRequest
é um candidato principal, mas, por enquanto,
vamos escrever uma função simples para fazer uma solicitação GET:
function get(url) {
// Return a new promise.
return new Promise(function(resolve, reject) {
// Do the usual XHR stuff
var req = new XMLHttpRequest();
req.open('GET', url);
req.onload = function() {
// This is called even on 404 etc
// so check the status
if (req.status == 200) {
// Resolve the promise with the response text
resolve(req.response);
}
else {
// Otherwise reject with the status text
// which will hopefully be a meaningful error
reject(Error(req.statusText));
}
};
// Handle network errors
req.onerror = function() {
reject(Error("Network Error"));
};
// Make the request
req.send();
});
}
Agora vamos usar:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
Agora podemos fazer solicitações HTTP sem digitar XMLHttpRequest
manualmente, o que é ótimo, porque quanto
menos eu tiver que ver a caixa de camelo irritante de XMLHttpRequest
, mais feliz será minha vida.
Encadeamento
then()
não é o fim da história. Você pode encadenar then
s para
transformar valores ou executar outras ações assíncronas uma após a outra.
Transformar valores
É possível transformar valores simplesmente retornando o novo valor:
var promise = new Promise(function(resolve, reject) {
resolve(1);
});
promise.then(function(val) {
console.log(val); // 1
return val + 2;
}).then(function(val) {
console.log(val); // 3
})
Como exemplo prático, vamos voltar ao:
get('story.json').then(function(response) {
console.log("Success!", response);
})
A resposta é JSON, mas estamos recebendo como texto simples. Poderíamos
mudar nossa função get para usar o JSON
responseType
,
mas também poderíamos resolvê-lo no mundo das promessas:
get('story.json').then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log("Yey JSON!", response);
})
Como JSON.parse()
usa um único argumento e retorna um valor transformado,
é possível criar um atalho:
get('story.json').then(JSON.parse).then(function(response) {
console.log("Yey JSON!", response);
})
Na verdade, podemos criar uma função getJSON()
com muita facilidade:
function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON()
ainda retorna uma promessa, que busca um URL e analisa
a resposta como JSON.
Enfileirar ações assíncronas
Também é possível encadear then
s para executar ações assíncronas em sequência.
Quando você retorna algo de um callback then()
, é um pouco mágico.
Se você retornar um valor, o próximo then()
será chamado com esse valor. No entanto,
se você retornar algo semelhante a uma promessa, o próximo then()
vai aguardar e só será
chamado quando essa promessa for resolvida (sucesso/falha). Exemplo:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
Aqui, fazemos uma solicitação assíncrona para story.json
, que fornece um conjunto de
URLs a serem solicitados. Em seguida, solicitamos o primeiro deles. É quando as promessas
realmente começam a se destacar dos padrões de callback simples.
Você pode até criar um método de atalho para acessar os capítulos:
var storyPromise;
function getChapter(i) {
storyPromise = storyPromise || getJSON('story.json');
return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})
}
// and using it is simple:
getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);
}).then(function(chapter) {
console.log(chapter);
})
Não fazemos o download de story.json
até que getChapter
seja chamado, mas na próxima
vez que getChapter
for chamado, vamos reutilizar a promessa de história, para que story.json
seja buscado apenas uma vez. Yay Promises!
Tratamento de erros
Como vimos anteriormente, then()
recebe dois argumentos, um para sucesso e outro
para falha (ou cumprir e rejeitar, em termos de promessas):
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
Também é possível usar catch()
:
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
Não há nada de especial em catch()
, é apenas uma forma mais simples de escrever
then(undefined, func)
, mas é mais fácil de ler. Os dois exemplos de código
acima não se comportam da mesma forma. O segundo é equivalente a:
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
A diferença é sutil, mas extremamente útil. As rejeições de promessas pulam
para a próxima then()
com um callback de rejeição (ou catch()
, já que
é equivalente). Com then(func1, func2)
, func1
ou func2
serão
chamados, nunca ambos. No entanto, com then(func1).catch(func2)
, ambos serão
chamados se func1
for rejeitado, já que são etapas separadas na cadeia. Faça
o seguinte:
asyncThing1().then(function() {
return asyncThing2();
}).then(function() {
return asyncThing3();
}).catch(function(err) {
return asyncRecovery1();
}).then(function() {
return asyncThing4();
}, function(err) {
return asyncRecovery2();
}).catch(function(err) {
console.log("Don't worry about it");
}).then(function() {
console.log("All done!");
})
O fluxo acima é muito semelhante ao try/catch normal do JavaScript. Erros que
acontecem em um "try" vão imediatamente para o bloco catch()
. Confira o
fluxograma acima:
Siga as linhas azuis para promessas que são cumpridas ou as linhas vermelhas para as que são rejeitadas.
Exceções e promessas do JavaScript
As rejeições acontecem quando uma promessa é rejeitada explicitamente, mas também implicitamente se um erro for gerado no callback do construtor:
var jsonPromise = new Promise(function(resolve, reject) {
// JSON.parse throws an error if you feed it some
// invalid JSON, so this implicitly rejects:
resolve(JSON.parse("This ain't JSON"));
});
jsonPromise.then(function(data) {
// This never happens:
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
Isso significa que é útil fazer todo o trabalho relacionado à promessa no callback do construtor de promessas para que os erros sejam detectados automaticamente e se tornem rejeições.
O mesmo vale para erros gerados em callbacks then()
.
get('/').then(JSON.parse).then(function() {
// This never happens, '/' is an HTML page, not JSON
// so JSON.parse throws
console.log("It worked!", data);
}).catch(function(err) {
// Instead, this happens:
console.log("It failed!", err);
})
Tratamento de erros na prática
Com nossa história e capítulos, podemos usar catch para mostrar um erro ao usuário:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
addHtmlToPage(chapter1.html);
}).catch(function() {
addTextToPage("Failed to show chapter");
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
Se a busca de story.chapterUrls[0]
falhar (por exemplo, http 500 ou o usuário estiver off-line),
ele vai pular todos os callbacks de sucesso a seguir, incluindo o que está em
getJSON()
, que tenta analisar a resposta como JSON, e também pular o
callback que adiciona chapter1.html à página. Em vez disso, ele passa para o callback
catch. Como resultado, a mensagem "Não foi possível mostrar o capítulo" será adicionada à página se
qualquer uma das ações anteriores falhar.
Como o try/catch do JavaScript, o erro é detectado e o código subsequente continua, então o indicador de carregamento está sempre oculto, que é o que queremos. O código acima se torna uma versão assíncrona não bloqueante de:
try {
var story = getJSONSync('story.json');
var chapter1 = getJSONSync(story.chapterUrls[0]);
addHtmlToPage(chapter1.html);
}
catch (e) {
addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'
Talvez você queira catch()
apenas para fins de registro, sem recuperar
o erro. Para fazer isso, basta gerar o erro novamente. Poderíamos fazer isso no
método getJSON()
:
function getJSON(url) {
return get(url).then(JSON.parse).catch(function(err) {
console.log("getJSON failed for", url, err);
throw err;
});
}
Conseguimos buscar um capítulo, mas queremos todos. Vamos fazer isso acontecer.
Paralelismo e sequenciamento: aproveitando o melhor dos dois
Pensar de forma assíncrona não é fácil. Se você estiver com dificuldades para começar, tente escrever o código como se ele fosse síncrono. Neste caso:
try {
var story = getJSONSync('story.json');
addHtmlToPage(story.heading);
story.chapterUrls.forEach(function(chapterUrl) {
var chapter = getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}
catch (err) {
addTextToPage("Argh, broken: " + err.message);
}
document.querySelector('.spinner').style.display = 'none'
Funcionou. Mas ele sincroniza e bloqueia o navegador enquanto as coisas são baixadas. Para
fazer isso funcionar de forma assíncrona, usamos then()
para que as coisas aconteçam uma após a outra.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
Mas como podemos percorrer os URLs dos capítulos e extrair os dados em ordem? Isso não funciona:
story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
})
O forEach
não é compatível com assíncrono, então nossos capítulos apareceriam na ordem
em que foram transferidos, que é basicamente como Pulp Fiction foi escrito. Isso não é
Pulp Fiction, então vamos corrigir.
Como criar uma sequência
Queremos transformar nossa matriz chapterUrls
em uma sequência de promessas. Isso pode ser feito usando then()
:
// Start off with a promise that always resolves
var sequence = Promise.resolve();
// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
// Add these actions to the end of the sequence
sequence = sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
})
Essa é a primeira vez que encontramos Promise.resolve()
, que cria uma
promessa que é resolvida com qualquer valor fornecido. Se você transmitir uma
instância de Promise
, ela será simplesmente retornada (observação:essa é uma
mudança na especificação que algumas implementações ainda não seguem). Se você
transmitir algo semelhante a uma promessa (tem um método then()
), ele vai criar um
Promise
genuíno que é cumprido/rejeitado da mesma maneira. Se você transmitir
qualquer outro valor, por exemplo, Promise.resolve('Hello')
, ele cria uma
promessa que é cumprida com esse valor. Se você chamar sem um valor,
como acima, ele será preenchido com "undefined".
Há também Promise.reject(val)
, que cria uma promessa que é rejeitada com
o valor fornecido (ou indefinido).
Podemos organizar o código acima usando
array.reduce
:
// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Add these actions to the end of the sequence
return sequence.then(function() {
return getJSON(chapterUrl);
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve())
Isso faz o mesmo que o exemplo anterior, mas não precisa da variável
"sequence" separada. Nosso callback de redução é chamado para cada item na matriz.
"sequence" é Promise.resolve()
na primeira vez, mas para o restante das
chamadas, "sequence" é o que retornamos da chamada anterior. array.reduce
é muito útil para reduzir uma matriz a um único valor, que, neste caso,
é uma promessa.
Vamos resumir:
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
return story.chapterUrls.reduce(function(sequence, chapterUrl) {
// Once the last chapter's promise is done…
return sequence.then(function() {
// …fetch the next chapter
return getJSON(chapterUrl);
}).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
// And we're all done!
addTextToPage("All done");
}).catch(function(err) {
// Catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
// Always hide the spinner
document.querySelector('.spinner').style.display = 'none';
})
E aí está, uma versão totalmente assíncrona da versão síncrona. Mas podemos fazer melhor. No momento, nossa página está sendo baixada assim:
Os navegadores são muito bons em fazer o download de várias coisas de uma só vez, então estamos perdendo performance ao fazer o download de capítulos um após o outro. O que queremos fazer é fazer o download de todos ao mesmo tempo e processá-los quando todos chegarem. Felizmente, há uma API para isso:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all
recebe uma matriz de promessas e cria uma promessa que é cumprida
quando todas são concluídas. Você recebe uma matriz de resultados (qualquer
promessa cumprida) na mesma ordem das promessas transmitidas.
getJSON('story.json').then(function(story) {
addHtmlToPage(story.heading);
// Take an array of promises and wait on them all
return Promise.all(
// Map our array of chapter urls to
// an array of chapter json promises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters) {
// Now we have the chapters jsons in order! Loop through…
chapters.forEach(function(chapter) {
// …and add to the page
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened so far
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
Dependendo da conexão, isso pode ser segundos mais rápido do que carregar um por um, e tem menos código do que nossa primeira tentativa. Os capítulos podem ser transferidos em qualquer ordem, mas aparecem na tela na ordem correta.
No entanto, ainda podemos melhorar a performance percebida. Quando o capítulo um chegar, ele será adicionado à página. Isso permite que o usuário comece a ler antes que o restante dos capítulos chegue. Quando o capítulo três chegar, não vamos adicioná-lo à página porque o usuário pode não perceber que o capítulo dois está faltando. Quando o segundo capítulo chegar, podemos adicionar os capítulos dois e três, etc.
Para fazer isso, buscamos o JSON de todos os capítulos ao mesmo tempo e criamos uma sequência para adicioná-los ao documento:
getJSON('story.json')
.then(function(story) {
addHtmlToPage(story.heading);
// Map our array of chapter urls to
// an array of chapter json promises.
// This makes sure they all download in parallel.
return story.chapterUrls.map(getJSON)
.reduce(function(sequence, chapterPromise) {
// Use reduce to chain the promises together,
// adding content to the page for each chapter
return sequence
.then(function() {
// Wait for everything in the sequence so far,
// then wait for this chapter to arrive.
return chapterPromise;
}).then(function(chapter) {
addHtmlToPage(chapter.html);
});
}, Promise.resolve());
}).then(function() {
addTextToPage("All done");
}).catch(function(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
}).then(function() {
document.querySelector('.spinner').style.display = 'none';
})
Pronto, o melhor dos dois mundos! O tempo para entregar todo o conteúdo é o mesmo, mas o usuário recebe o primeiro pedaço de conteúdo mais cedo.
Neste exemplo simples, todos os capítulos chegam mais ou menos ao mesmo tempo, mas o benefício de mostrar um de cada vez será exagerado com capítulos maiores.
Fazer o que foi mencionado acima com callbacks ou eventos no estilo do Node.js é quase o dobro do código, mas o mais importante é que não é tão fácil de seguir. No entanto, isso não é o fim da história para as promessas. Quando combinadas com outros recursos do ES6, elas ficam ainda mais fáceis.
Rodada bônus: recursos avançados
Desde que escrevi este artigo pela primeira vez, a capacidade de usar promessas aumentou muito. Desde o Chrome 55, as funções assíncronas permitem que o código baseado em promessas seja escrito como se fosse síncrono, mas sem bloquear a linha de execução principal. Leia mais sobre isso no artigo sobre funções assíncronas. Há um amplo suporte para promessas e funções assíncronas nos principais navegadores. Confira os detalhes na referência Promise e função assíncrona do MDN.
Agradecemos a Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans e Yutaka Hirano, que revisaram este documento e fizeram correções/recomendações.
Agradecemos a Mathias Bynens por atualizar várias partes do artigo.