As promessas simplificam 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.
[Começa o rufar de tambores]
As promessas chegaram ao JavaScript!
[Fogos de artifício explodem, papéis brilhantes caem do céu, a multidão vibra]
Nesse ponto, você se enquadra em uma destas categorias:
- As pessoas estão torcendo ao seu redor, mas você não sabe o que está acontecendo. Talvez você nem saiba o que é uma "promessa". Você daria de ombros, mas o peso do papel brilhante está sobre seus ombros. Se sim, não se preocupe. Levei muito tempo para entender por que deveria me importar com essas coisas. É recomendável começar pelo início.
- Você soca o ar! Já tava na hora, né? Você já usou essas coisas de promessas antes, mas se incomoda com o fato de que todas as implementações têm uma API ligeiramente diferente. Qual é a API da versão oficial em JavaScript? Recomendamos começar com a terminologia.
- Você já sabia disso e zomba daqueles que estão pulando como se fosse uma novidade. Aproveite um momento para se sentir superior e acesse a referência da API.
Suporte e polyfill do navegador
Para deixar os navegadores que não têm uma implementação completa de promessas em conformidade com as especificações ou adicionar promessas a outros navegadores e ao Node.js, confira o polyfill (2k gzipped).
Por que tanto alvoroço?
O JavaScript é de linha ú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 várias outras coisas que variam de navegador para navegador. No entanto, 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 um deles atrasa os outros.
Como ser humano, você é multithread. Você pode digitar com vários dedos, dirigir e conversar ao mesmo tempo. A única função de bloqueio com que precisamos lidar é o espirro, em que toda a atividade atual precisa ser suspensa durante a duração do espirro. Isso é bem irritante, principalmente quando você está dirigindo e tentando manter uma conversa. Você não quer escrever um código que seja espirrado.
Você provavelmente já usou eventos e callbacks para contornar isso. 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 de espirrar. 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 ocorrido antes de começarmos a escutar, então 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 detecta imagens que apresentaram erros antes de termos a chance de ouvi-las. Infelizmente, o DOM não oferece uma maneira de fazer isso. Além disso, isso 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 maneira
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 assim:
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 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
});
Basicamente, as promessas são um pouco parecidas com listeners de eventos, mas:
- Uma promessa só pode ser concluída ou falhar uma vez. Ela não pode ter sucesso ou falhar duas vezes, nem mudar de sucesso para falha ou vice-versa.
- Se uma promessa for bem-sucedida ou falhar e você adicionar um callback de sucesso/falha depois, o callback correto será chamado, mesmo que o evento tenha ocorrido antes.
Isso é extremamente útil para sucesso/falha assíncronos, porque você tem menos interesse no momento exato em que algo ficou disponível e mais interesse em reagir ao resultado.
Terminologia de promessa
Domenic Denicola revisou a primeira versão deste artigo e me deu uma nota "F" por terminologia. Ele me deixou de castigo, me obrigou a copiar Estados e Destinos 100 vezes e escreveu uma carta preocupada para meus pais. Apesar disso, ainda confundo muito 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 atendido nem recusado.
- settled: foi atendido ou rejeitado.
A especificação
também usa o termo thenable para descrever um objeto semelhante a uma promessa,
já que ele tem um método then
. Esse termo me lembra o 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 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ê usa o jQuery, ele tem algo semelhante chamado Deferreds. No entanto, os objetos Deferred não são compatíveis com Promise/A+, o que os torna ligeiramente diferentes e menos úteis. Portanto, tome cuidado. O jQuery também tem um tipo Promise, mas esse é apenas um subconjunto de Deferred e tem os mesmos problemas.
Embora as implementações de promessas sigam um comportamento padronizado, as APIs gerais são diferentes. As promessas do JavaScript são semelhantes na API ao 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 promessas usa um argumento, um callback com dois parâmetros: "resolve" e "reject". Faça algo no callback, talvez de forma assíncrona, e chame resolve se tudo funcionar. Caso contrário, chame reject.
Assim como throw
em JavaScript simples, é comum, mas não obrigatório, rejeitar com um objeto de erro. O benefício dos objetos de erro é que eles capturam um
stack trace, o que torna 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 o caso de falha. Os dois 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, foram movidas para o JavaScript. Ter essas APIs em JavaScript em vez do DOM é ótimo porque elas estarão disponíveis em contextos JS que não são de navegador, como Node.js. Se elas usam essas APIs nas principais APIs, é 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 assíncronos de sucesso/falha vão usar promessas. Isso já está acontecendo com Gerenciamento de cotas, Eventos de carregamento de fontes, ServiceWorker, Web MIDI, Streams e muito mais.
Compatibilidade com outras bibliotecas
A API JavaScript Promises trata qualquer coisa com um método then()
como semelhante a uma promessa (ou thenable
, em linguagem de promessas suspiro). Portanto, se você usar uma biblioteca que retorne uma promessa Q, não há problema. Ela vai funcionar bem com as novas promessas JavaScript.
No entanto, como mencionei, os adiamentos do jQuery são um pouco… inúteis. Felizmente, é possível transmitir essas promessas para promessas padrão, o que vale a pena fazer o mais rápido 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 objetos 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, 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
Vamos programar algumas coisas. Digamos que queremos:
- Iniciar um spinner para indicar o carregamento
- Extrai um JSON de uma história, que nos dá 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 spinner
… mas também informe ao usuário se algo deu errado no processo. Também vamos querer parar o spinner nesse ponto, caso contrário, ele vai continuar girando, ficar tonto e colidir com alguma outra interface.
É claro que você não usaria JavaScript para veicular uma história, já que servir como HTML é mais rápido, mas esse padrão é bastante comum ao lidar com APIs: várias buscas de dados e, em seguida, fazer algo quando tudo estiver pronto.
Para começar, vamos buscar dados da rede:
Promisificar XMLHttpRequest
As APIs antigas serão atualizadas para usar promessas, se isso for possível de maneira compatível com versões anteriores. XMLHttpRequest
é um ótimo 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 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 manualmente XMLHttpRequest
, o que é ótimo, porque quanto menos eu tiver que ver o camel-case irritante de XMLHttpRequest
, mais feliz minha vida será.
Encadeamento
then()
não é o fim da história. Você pode encadear then
s para transformar valores ou executar outras ações assíncronas uma após a outra.
Transformar valores
Para transformar valores, basta retornar 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 a:
get('story.json').then(function(response) {
console.log("Success!", response);
})
A resposta é JSON, mas no momento estamos recebendo como texto simples. Podemos alterar nossa função "get" para usar o JSON responseType
, mas também podemos resolver isso com 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.
Enfileirar ações assíncronas
Você também pode 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 esperar por ele e só será
chamado quando essa promessa for concluída (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. É quando 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 na próxima vez que getChapter
for chamado, vamos reutilizar a promessa de história. Assim, story.json
só será buscado uma vez. Viva as promessas!
Tratamento de erros
Como vimos antes, then()
usa dois argumentos, um para sucesso e outro para
falha (ou fulfill e reject, em termos 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 legível de then(undefined, func)
. Os dois exemplos de código acima não se comportam da mesma forma. O último é 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 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ão
chamados, nunca ambos. Mas com then(func1).catch(func2)
, ambos serão
chamados se func1
rejeitar, já que são etapas separadas na cadeia. Considere 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
acontecem em um "try" vão imediatamente para o bloco catch()
. Confira o diagrama de fluxo acima (porque eu adoro diagramas de fluxo):
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 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 a promessas 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 o usuário estiver off-line),
todos os callbacks de sucesso a seguir serão ignorados, 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
de captura. Como resultado, a mensagem "Não foi possível mostrar o capítulo" será adicionada à página se
alguma das ações anteriores falhar.
Assim como o try/catch do JavaScript, o erro é capturado e o código subsequente continua. Portanto, o spinner sempre fica oculto, que é o que queremos. O código acima se torna uma versão assíncrona não bloqueadora 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 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
Pensar de forma assíncrona não é fácil. Se você estiver com dificuldades para começar, tente escrever o código como se 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 os itens são baixados. 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 fazer um loop nas URLs dos capítulos e buscá-los 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 tem reconhecimento assíncrono, então os capítulos aparecem na ordem em que são 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ê atribua a ela. Se você transmitir uma instância de Promise
, ela será retornada (observação:essa é uma mudança na especificação que algumas implementações ainda não seguem). Se você passar algo semelhante a uma promessa (tem um método then()
), ele vai criar um Promise
genuíno que será cumprido/rejeitado da mesma forma. 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 rejeitada com o valor que você atribui a ela (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 separada "sequence". 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, nesse 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 de uma vez, 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, há 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 (qualquer que seja o número de promessas cumpridas) 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 é menos código do que nossa primeira tentativa. Os capítulos podem ser baixados em qualquer ordem, mas aparecem na tela na ordem correta.
No entanto, ainda podemos melhorar o desempenho percebido. Quando o primeiro capítulo 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 o adicione à 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. 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! Leva o mesmo tempo para entregar todo o conteúdo, mas o usuário recebe a primeira parte antes.
Neste exemplo simples, todos os capítulos chegam mais ou menos ao mesmo tempo, mas o benefício de mostrar um por vez será exagerado com mais capítulos e maiores.
Fazer o acima com callbacks ou eventos no estilo Node.js é cerca do dobro do código, mas, mais importante, não é tão fácil de seguir. No entanto, essa não é a história completa das promessas. 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 artigo sobre funções assíncronas. Há suporte generalizado para Promises e funções assíncronas nos principais navegadores. Confira os detalhes nas referências de Promise e async function da MDN.
Agradecemos a Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans e Yutaka Hirano, que revisaram este artigo e fizeram correções/recomendações.
Agradecemos também a Mathias Bynens por atualizar várias partes do artigo.