As promessas simplificam os cálculos adiados e assíncronos. 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 web.
[Rufam os tambores]
As promessas chegaram ao JavaScript!
[Fogos de artifício explodem, papel brilhante cai do céu, a multidão vai à loucura]
Neste ponto, você se enquadra em uma destas categorias:
- As pessoas estão comemorando ao seu redor, mas você não sabe o motivo de tanta animação. Talvez você nem tenha certeza do que é uma "promessa". Você daria de ombros, mas o peso do papel brilhante está pesando sobre seus ombros. Se sim, não se preocupe. Levei muito tempo para entender por que eu deveria me importar com isso. É recomendável começar do início.
- Você dá um soco no ar! Já estava na hora, né? Você já usou essas Promises antes, mas se incomoda com o fato de que todas as implementações têm uma API um pouco diferente. Qual é a API da versão oficial do JavaScript? É recomendável começar com a terminologia.
- Você já sabia disso e zomba de quem está pulando de alegria como se fosse novidade. Aproveite o momento para se orgulhar da sua superioridade e acesse diretamente a referência da API.
Suporte ao navegador e polyfill
Para que os navegadores que não têm uma implementação completa de promessas estejam em conformidade com as especificações ou para adicionar promessas a outros navegadores e ao Node.js, confira o polyfill (2k gzipped).
What's all the fuss about?
O JavaScript é de linha de execução única, 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 uma série de outras coisas que variam de navegador para navegador. Mas, normalmente, o JavaScript está na mesma fila que a pintura, a atualização de estilos e o processamento de ações do usuário, como destacar texto e interagir com controles de formulário. A atividade em uma dessas coisas atrasa as outras.
Como ser humano, você é multithread. Você pode digitar com vários dedos, pode dirigir e conversar ao mesmo tempo. A única função de bloqueio com que precisamos lidar é o espirro, em que todas as atividades atuais precisam ser suspensas durante o espirro. Isso é bem chato, especialmente quando você está dirigindo e tentando conversar. Você não quer escrever um código que seja "espirro".
Você provavelmente já usou eventos e callbacks para contornar isso. Aqui estã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 espirrento. Recebemos a imagem, adicionamos alguns listeners e, em seguida, o JavaScript pode interromper a execução até que um desses listeners seja chamado.
Infelizmente, no exemplo acima, é possível que os eventos tenham acontecido antes de começarmos a detectá-los. Por isso, precisamos contornar isso 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 apresentaram erro antes de termos a chance de detectá-las. Infelizmente, o DOM não oferece uma maneira de fazer isso. Além disso, estamos 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, você não se importa
com o que aconteceu antes de anexar o listener. Mas, quando se trata de
sucesso/falha assíncrona, o ideal é algo como:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
É isso que as promessas fazem, mas com uma nomenclatura melhor. 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
});
Em sua forma mais básica, as promessas são um pouco como listeners de eventos, exceto que:
- Uma promessa só pode ser bem-sucedida ou falhar uma vez. Ele não pode ser bem-sucedido ou falhar duas vezes, nem pode mudar de sucesso para falha ou vice-versa.
- Se uma promessa for bem-sucedida ou falhar e você adicionar um callback de sucesso/falha mais tarde, o callback correto será chamado, mesmo que o evento tenha ocorrido antes.
Isso é extremamente útil para sucesso/falha assíncrona, porque você não está tão interessado no momento exato em que algo ficou disponível, mas sim em reagir ao resultado.
Terminologia de promessa
Domenic Denicola revisou o primeiro rascunho deste artigo e me deu nota "F" em terminologia. Ele me deixou de castigo, me forçou a copiar Estados e Destinos 100 vezes e escreveu uma carta preocupada para meus pais. Apesar disso, ainda confundo muitos termos, mas aqui está o básico:
Uma promessa pode ser:
- fulfilled: a ação relacionada à promessa foi bem-sucedida.
- rejeitada: a ação relacionada à promessa falhou.
- pending: ainda não foi atendido ou rejeitado.
- settled: foi atendida ou rejeitada.
A especificação
também usa o termo thenable para descrever um objeto que é semelhante a uma promessa,
porque tem um método then. Esse termo me lembra o ex-técnico de futebol da Inglaterra, Terry Venables, então vou usá-lo o mínimo possível.
As promessas chegaram 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ê é usuário do jQuery, eles têm algo semelhante chamado Deferreds. No entanto, os Deferreds não são compatíveis com Promise/A+, o que os torna sutilmente diferentes e menos úteis. Portanto, tenha cuidado. O jQuery também tem um tipo de Promise, mas é apenas um subconjunto de Deferred e tem os mesmos problemas.
Embora as implementações de promessa sigam um comportamento padronizado, as APIs gerais delas são diferentes. As promessas do JavaScript são semelhantes à API 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 de promessa usa um argumento, um callback com dois parâmetros, resolve e reject. 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 de erro é que eles capturam um
stack trace, tornando as ferramentas de depuração mais úteis.
Veja como usar essa promessa:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then() usa dois argumentos: um callback para um caso de sucesso e outro para um 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, por fim, movidas para o JavaScript. Ter esses recursos em JavaScript em vez do DOM é ótimo porque eles estarão disponíveis em contextos JS que não são do navegador, como Node.js (se eles os usam nas APIs principais é outra questão).
Embora sejam um recurso do JavaScript, o DOM não tem medo de usá-las. Na verdade, todas as novas APIs DOM com métodos assíncronos de sucesso/falha usarão promessas. Isso já está acontecendo com Quota Management, Font Load Events, ServiceWorker, Web MIDI, Streams e muito mais.
Compatibilidade com outras bibliotecas
A API de promessas do JavaScript trata qualquer coisa com um método then() como
semelhante a uma promessa (ou thenable na linguagem de promessas suspiro). 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ê-los 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(),
o 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) {
// ...
})
Já as promessas de JS ignoram todas as chamadas, 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 simplificado
Certo, vamos programar algumas coisas. Digamos que queremos:
- Iniciar um ícone de carregamento para indicar o carregamento
- Busca 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 matéria à página
- Parar o controle de rotação
… mas também informar ao usuário se algo deu errado no processo. Também queremos interromper o ícone de carregamento nesse ponto. Caso contrário, ele vai continuar girando, ficar tonto e colidir com outra interface.
Claro que você não usaria JavaScript para entregar uma história, já que a exibição como HTML é mais rápida, mas esse padrão é bastante comum ao lidar com APIs: várias buscas de dados e, em seguida, uma ação quando tudo estiver pronto.
Para começar, vamos buscar dados da rede:
Promisifying XMLHttpRequest
As APIs antigas serão atualizadas para usar promessas, se possível, de uma forma compatível com versões anteriores. XMLHttpRequest é um excelente candidato, mas, enquanto isso,
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 usá-lo:
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 as letras concatenadas irritantes de XMLHttpRequest, mais feliz será minha vida.
Encadeamento
then() não é o fim da história. É possível encadear thens para
transformar valores ou executar outras ações assíncronas uma após a outra.
Transformação de 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 para:
get('story.json').then(function(response) {
console.log("Success!", response);
})
A resposta é JSON, mas no momento estamos recebendo como texto simples. Poderíamos
alterar nossa função get para usar o JSON
responseType,
mas também poderíamos resolver isso no ambiente de 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,
podemos 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.
Como colocar ações assíncronas em fila
Também é possível encadear thens 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 (com 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 nos dá um conjunto de
URLs para solicitar. Em seguida, solicitamos o primeiro deles. É aí que as promessas
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 nas próximas
vezes que getChapter for chamado, reutilizaremos a promessa da história, então story.json
será buscado apenas uma vez. Viva as promessas!
Tratamento de erros
Como vimos anteriormente, then() usa dois argumentos, um para sucesso e outro para falha (ou fulfill e reject, na linguagem de promessas):
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
Você também pode 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
then(undefined, func), mas é mais legível. 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 promessa avançam
para o próximo then() com um callback de rejeição (ou catch(), já que
é equivalente). Com then(func1, func2), func1 ou func2 será
chamado, nunca ambos. Mas, com then(func1).catch(func2), ambos serão
chamados se func1 rejeitar, já que são etapas separadas na cadeia. Tome
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. Os erros que
ocorrem em um "try" vão imediatamente para o bloco catch(). Veja o
fluxograma acima (porque eu adoro fluxogramas):
Siga as linhas azuis para promessas que são cumpridas ou as vermelhas para as que são rejeitadas.
Exceções e promessas de JavaScript
As rejeições acontecem quando uma promessa é explicitamente rejeitada, mas também implicitamente se um erro é 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 dentro do callback do construtor de promessas, para que os erros sejam capturados 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 usuário off-line),
ela vai pular todos os callbacks de sucesso seguintes, incluindo o de
getJSON(), que tenta analisar a resposta como JSON, e também o
callback que adiciona chapter1.html à página. Em vez disso, ele passa para o callback
catch. Como resultado, "Falha ao mostrar o capítulo" será adicionado à página se
alguma das ações anteriores falhar.
Como o try/catch do JavaScript, o erro é capturado e o código subsequente continua, então o ícone de carregamento sempre fica oculto, que é o que queremos. O exemplo 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 usar catch() apenas para fins de registro, sem se recuperar
do erro. Para fazer isso, basta gerar o erro novamente. Podemos 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: o melhor dos dois mundos
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'
Pode ser! Mas é síncrono e bloqueia o navegador enquanto os downloads são feitos. Para
que isso funcione 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 buscá-los em ordem? Não funciona:
story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
})
forEach não reconhece assincronia, então nossos capítulos apareceriam em qualquer ordem
em que fossem baixados, 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. Podemos fazer isso 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);
});
})
Esta é a primeira vez que vemos Promise.resolve(), que cria uma
promessa que é resolvida para qualquer valor que você fornecer. 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 (com um método then()), ele vai criar uma
Promise genuína que é cumprida/rejeitada da mesma forma. Se você transmitir
qualquer outro valor, por exemplo, Promise.resolve('Hello'), ele vai criar uma
promessa que será cumprida com esse valor. Se você chamá-la sem valor,
como acima, ela será atendida com "undefined".
Há também Promise.reject(val), que cria uma promessa que rejeita 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. O 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 juntar tudo:
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 pronto, temos 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 baixar várias coisas ao mesmo tempo, então estamos perdendo desempenho ao baixar os capítulos um após o outro. O que queremos fazer é baixar todos ao mesmo tempo e processá-los quando todos chegarem. Felizmente, existe uma API para isso:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all usa uma matriz de promessas e cria uma promessa que é cumprida
quando todas são concluídas. Você recebe uma matriz de resultados (seja qual for
o resultado das promessas) na mesma ordem das promessas que você transmitiu.
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, além de exigir menos código do que nossa primeira tentativa. Os capítulos podem ser baixados em qualquer ordem, mas aparecem na tela na ordem certa.
No entanto, ainda podemos melhorar o desempenho percebido. Quando o capítulo um chegar, vamos adicioná-lo à 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 capítulo dois 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';
})
E pronto, o melhor dos dois mundos! Leva o mesmo tempo para entregar todo o conteúdo, mas o usuário recebe o primeiro conteúdo mais cedo.
Neste exemplo simples, todos os capítulos chegam mais ou menos ao mesmo tempo, mas o benefício de exibir um por vez será maior com mais capítulos maiores.
Fazer o mesmo com callbacks ou eventos no estilo Node.js (link em inglês) exige o dobro de código, mas, o mais importante, não é tão fácil de acompanhar. No entanto, as promessas não param por aí. Quando combinadas com outros recursos do ES6, elas ficam ainda mais fáceis.
Rodada bônus: recursos ampliados
Desde que escrevi este artigo, a capacidade de usar Promises 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 meu artigo sobre funções assíncronas. Há suporte amplo para Promises e funções assíncronas nos principais navegadores. Confira os detalhes na referência da MDN sobre Promise e função assíncrona.
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.
Além disso, agradecemos a Mathias Bynens por atualizar várias partes do artigo.