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 da Web.
[O tambor começa]
As promessas chegaram ao JavaScript!
[Fogos de artifício explodem, papel brilhante chove, a multidão enlouquece]
Nesse ponto, você se enquadra em uma destas categorias:
- As pessoas estão comemorando ao seu redor, mas você não tem certeza do que se trata. Talvez você não tenha certeza do que é uma "promessa". Você encolheria, mas o peso do papel brilhante fica muito sobre seus ombros. Se for assim, não se preocupe com isso. Demorei uma eternidade para entender por que eu deveria me importar com essas coisas. É recomendável começar pelo início.
- Você soca o ar! Já tava na hora, não é? Você já usou essas promessas antes, mas está incomodado 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 ridiculariza aqueles que estão pulando como se fosse novidade para eles. Curta sua superioridade por um momento e acesse a Referência da API.
Compatibilidade com navegadores e polyfill
Para ajustar os navegadores que não têm uma implementação completa de promessas à conformidade com as especificações ou adicionar promessas a outros navegadores e ao Node.js, confira o polyfill (em inglês) (2 mil arquivos .zip).
Por que todo esse estardalhaço?
O JavaScript tem uma linha de execução única, o que significa que duas partes de um script não podem ser executadas ao mesmo tempo. Elas precisam ser executadas uma após a outra. Em navegadores, o JavaScript compartilha uma thread com muitas 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 destaque de texto e interação com controles de formulários. A atividade em uma dessas coisas atrasa as outras.
Você, como ser humano, usa várias linhas de execução. Você pode digitar com vários dedos, dirigir e conversar ao mesmo tempo. A única função de bloqueio com que temos de lidar é o espirro, em que todas as atividades em andamento precisam ser suspensas pela duração dele. Isso é muito irritante, especialmente quando você está dirigindo e tentando manter uma conversa. Você não quer escrever um código que espirre.
Você provavelmente já usou eventos e callbacks para contornar isso. Veja os eventos:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
Não há nenhum espirro. Obtemos a imagem, adicionamos alguns listeners e o JavaScript pode parar a execução até que um deles seja chamado.
Infelizmente, no exemplo acima, é possível que os eventos tenham acontecido antes de começarmos a ouvi-los. Portanto, precisamos contornar isso usando a propriedade "completa" 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 as imagens que apresentaram erros antes de serem ouvidos. Infelizmente, o DOM não permite fazer isso. Além disso, uma imagem é carregada. Tudo fica ainda mais complexo quando queremos saber quando um conjunto de imagens foi carregado.
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 você anexar o listener. Mas quando se trata de sucesso/falha assíncronos, 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 é o que as promessas fazem, mas com nomes melhores. Se os elementos de uma imagem HTML tivessem um método "ready" que retornasse uma promessa, poderíamos fazer isto:
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:
- Uma promessa só pode ser bem-sucedida ou falhar uma vez. Ela não pode ser bem-sucedida ou falhar duas vezes, nem alterna entre sucesso e falha ou vice-versa.
- Se uma promessa tiver sido concluída ou falhar e você adicionar posteriormente um retorno de chamada de sucesso/falha, o retorno de chamada correto será chamado, mesmo que o evento tenha ocorrido anteriormente.
Isso é extremamente útil para sucesso/falha assíncronos, porque você está menos interessado no momento exato em que algo ficou disponível e mais interessado em reagir ao resultado.
Terminologia das promessas
Domenic Denicola leu o primeiro rascunho deste artigo e me avaliou com "F" quanto à terminologia. Ele me colocou em prisão, me obrigou a copiar Estados e destinos 100 vezes e escreveu uma carta preocupada para meus pais. Apesar disso, eu ainda me confundo com a terminologia, mas estes são os princípios básicos:
Uma promessa pode ser:
- atendida: a ação relacionada à promessa foi bem-sucedida.
- rejected: a ação relacionada à promessa falhou
- pendente: ainda não foi atendida nem rejeitada.
- resolvido: foi atendido ou recusado
A especificação
também usa o termo thenable para descrever um objeto semelhante à promessa,
já que tem um método then
. Esse termo me lembra o ex-dirigente do futebol inglês,
Terry Venables, então
vou usá-lo o mínimo possível.
As promessas chegaram em JavaScript!
As promessas já existem há algum tempo na forma de bibliotecas, como:
As promessas do JavaScript e acima compartilham um comportamento comum e padronizado, chamado Promises/A+. Se você usa jQuery, elas têm algo semelhante chamado Deferreds. No entanto, os diferidos não são compatíveis com Promise/A+, o que os torna sutilmente diferentes e menos úteis, então, cuidado. O jQuery também tem um tipo de promessa, mas é apenas um subconjunto de Deferreds e apresenta os mesmos problemas.
As implementações de promessas seguem um comportamento padronizado, mas as APIs gerais são diferentes. As promessas do JavaScript são semelhantes na API ao RSVP.js. Confira 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 aceita um argumento, um callback com dois parâmetros, resolver e rejeitar. Faça algo dentro do callback, talvez assíncrono, e chame "resolver" se tudo funcionou bem. Caso contrário, chame "rejeitar".
Assim como o throw
no JavaScript simples, é comum, mas não obrigatório, rejeitar com um objeto Error. A vantagem dos objetos Error é 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 o sucesso e outro para a falha. Ambos são opcionais. Portanto, é possível adicionar um callback apenas para a
conclusão ou apenas para a falha.
As promessas do JavaScript começaram no DOM como "Futures", foram renomeadas para "Promises" e, finalmente, movidas para o JavaScript. É ótimo ter esses elementos em JavaScript, e não no DOM, porque eles ficam disponíveis em contextos JS fora do navegador, como Node.js, independentemente de serem usados nas APIs principais.
Embora sejam um recurso JavaScript, o DOM não tem medo de usá-los. Na verdade, todas as novas APIs do DOM com métodos de sucesso/falha usarão promessas. Isso já está acontecendo com o Gerenciamento de cotas, Eventos de carregamento de fonte, ServiceWorker, Web MIDI, Streams e muito mais.
Compatibilidade com outras bibliotecas
A API de promessas do JavaScript vai tratar tudo 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 do Q, tudo bem, ela funcionará bem com as novas
promessas do JavaScript.
Embora, como mencionei, os Deferreds do jQuery sejam um pouco... inúteis. Felizmente, é possível convertê-los em promessas padrão, o que é bom 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()
,
Promise.resolve()
pode transformá-lo em uma promessa de JavaScript. No entanto,
às vezes, os diferidos transmitem vários argumentos aos callbacks deles, 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, esteja ciente de que a jQuery não segue a convenção de transmitir objetos Error para rejeições.
Simplificar o código assíncrono complexo
Vamos programar algumas coisas. Digamos que você queira:
- Iniciar um ícone de carregamento para indicar o carregamento
- Buscar algum JSON para uma história que forneça o título e 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 informar ao usuário se algo deu errado no caminho. Queremos interromper o ícone de carregamento nesse ponto também. Caso contrário, ele vai continuar a girar, ficar tonto e falhar em outra interface.
Obviamente, você não usaria JavaScript para enviar uma história, veicular como HTML é mais rápido, mas esse padrão é muito comum ao lidar com APIs: faça várias buscas de dados e faça algo quando tudo estiver pronto.
Para começar, vamos buscar dados da rede:
Promissão de XMLHttpRequest
As APIs antigas serão atualizadas para usar promessas, se for possível, de maneira compatível com versões anteriores. XMLHttpRequest
é um candidato importante, 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á-la:
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 irritante estilo CamelCase de XMLHttpRequest
, mais feliz será minha vida.
Encadeamento
O then()
não é o fim da história. Você pode encadear then
s para
transformar valores ou executar mais ações assíncronas uma após a outra.
Como 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 para:
get('story.json').then(function(response) {
console.log("Success!", response);
})
A resposta é JSON, mas o estamos recebendo no momento como texto simples. Poderíamos alterar nossa função get para usar o JSON responseType
, mas também podemos resolvê-la em 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, é muito fácil criar uma função getJSON()
:
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 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()
, funciona como mágica.
Se você retornar um valor, a próxima then()
será chamada com esse valor. No entanto, se você retornar algo semelhante a uma promessa, o próximo then()
ficará aguardando e será chamado apenas quando essa promessa for definida (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 fornece um conjunto de
URLs a serem solicitados. Em seguida, solicitamos o primeiro deles. É aí que as promessas começam a se destacar de simples padrões de callback.
Você pode até criar um método de atalho para acessar 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 da história. Portanto, story.json
será buscado apenas uma vez. Eba!
Tratamento de erros
Como vimos anteriormente, then()
usa dois argumentos, um para sucesso e outro
para falha (ou atender e rejeitar, na linguagem 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 outra forma de
then(undefined, func)
, mas é mais legível. Observe que 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 promessa pulam
para o próximo then()
com um callback de rejeição (ou catch()
, porque
é equivalente). Com then(func1, func2)
, serão
chamadas func1
ou func2
, nunca ambas. No entanto, com then(func1).catch(func2)
, ambas serão
chamadas se func1
for rejeitada, já que são etapas separadas na cadeia. Veja
o exemplo a seguir:
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()
. Veja a seguir o fluxograma acima (porque adoro fluxogramas):
Siga as linhas azuis para promessas atendidas ou vermelhas para as 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 a promessas dentro do callback do construtor de promessas, para que os erros sejam detectados automaticamente e se transformem em 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);
})
Como lidar com erros na prática
Com nossa história e capítulos, podemos usar "catch" para exibir 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 está off-line),
ela vai pular todos os callbacks de sucesso seguintes, incluindo o de
getJSON()
, que tenta analisar a resposta como JSON. Além disso, ele ignora o
callback que adiciona capítulo1.html à página. Em vez disso, ela se move para o callback "catch". Como resultado, se
qualquer uma das ações anteriores falhar, "Falha ao mostrar o capítulo" será adicionado à página.
Assim como o try/catch do JavaScript, o erro é capturado e o código subsequente continua. Portanto, o ícone de carregamento fica sempre oculto, que é o que queremos. O código acima se torna uma versão assíncrona sem bloqueio do seguinte:
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'
O catch()
pode ser usado simplesmente para fins de geração de registros, sem recuperação
do erro. Para fazer isso, gere o erro novamente. Podemos fazer isso em
nosso método getJSON()
:
function getJSON(url) {
return get(url).then(JSON.parse).catch(function(err) {
console.log("getJSON failed for", url, err);
throw err;
});
}
Portanto, conseguimos buscar um capítulo, mas queremos todos eles. Vamos fazer isso acontecer.
Paralelismo e sequência: como aproveitar ao máximo os dois recursos
Pensar de forma assíncrona não é fácil. Se você está 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 é sincronizado e bloqueia o navegador durante os downloads. Para
fazer isso de maneira 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? 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 reconhece assincronia, então os capítulos apareceriam na ordem de download, que é basicamente a forma como Pulp curtas foi escrito. Isso não é
Pulp Ficção, 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 o Promise.resolve()
, que cria uma
promessa que é resolvida para qualquer valor informado. Se você transmitir uma
instância de Promise
, ela vai retornar o valor. 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()
), ela vai criar uma
Promise
genuína que é atendida/rejeitada da mesma maneira. Se você transmitir qualquer outro valor, por exemplo, Promise.resolve('Hello')
, ele cria uma promessa que será atendida com esse valor. Se você fizer a chamada sem um valor, como acima, a função será atendida com "indefinido".
Existe também Promise.reject(val)
, que cria uma promessa que será 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())
Esse código faz o mesmo que o exemplo anterior, mas não precisa da variável separada "sequência". Nosso callback reduzido é chamado para cada item na matriz.
"sequência" é Promise.resolve()
na primeira vez, mas, para o restante das chamadas, "sequência" é aquilo que retornamos da chamada anterior. array.reduce
é muito útil para resumir uma matriz a um único valor, que, nesse caso,
é uma promessa.
Como reunir 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 aí está, uma versão totalmente assíncrona da versão sincronizada. Mas podemos fazer melhor. No momento, o download da página é feito assim:
Os navegadores são muito bons em fazer o download de vários itens de uma só vez. Por isso, estamos perdendo desempenho 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 forem concluídos. Felizmente, existe uma API para isso:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all
recebe uma matriz de promessas e cria uma promessa que será atendida
quando todas elas forem concluídas com sucesso. Você recebe uma matriz de resultados (com base nas promessas atendidas) 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 alguns segundos mais rápido do que o carregamento individual, e o código fica menor do que na primeira tentativa. É possível fazer o download dos capítulos em qualquer ordem, mas eles aparecem na tela na ordem correta.
No entanto, ainda podemos melhorar o desempenho percebido. Quando o capítulo um chega, precisamos adicioná-lo à página. Isso permite que o usuário comece a ler antes da chegada dos outros capítulos. Quando o capítulo três chegar, não o adicionaremos à página porque o usuário pode não perceber que o capítulo dois está ausente. Quando o capítulo dois chegar, podemos adicionar os capítulos dois, três e assim por diante.
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 aí está, o melhor dos dois! O mesmo tempo é necessário para entregar todo o conteúdo, mas o usuário recebe a primeira parte dele mais cedo.
Nesse exemplo trivial, todos os capítulos chegam aproximadamente no mesmo horário, mas o benefício de exibir um de cada vez será exagerado com capítulos mais maiores.
Fazer isso com callbacks ou eventos no estilo Node.js envolve o dobro do código, mas, o mais importante, não é tão fácil de acompanhar. No entanto, esse não é o fim do caso sobre promessas. Quando combinadas com outros recursos do ES6, elas ficam ainda mais fáceis.
Rodada bônus: recursos expandidos
Desde que escrevi este artigo, a capacidade de usar promessas se expandiu bastante. Desde o Chrome 55, as funções assíncronas permitem que códigos baseados em promessas sejam escritos como se fossem síncronos, mas sem bloquear a linha de execução principal. Leia mais sobre isso no my async functions article. Há um suporte generalizado para promessas e funções assíncronas nos principais navegadores. Você encontra os detalhes na referência de Promise e na função assíncrona do MDN (links em inglês).
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 também a Mathias Bynens por atualizar várias partes do artigo.