// With Promises | |
getJson('story.json').then(function(story) { | |
addHtmlToPage(story.heading); | |
// Map our array of chapter urls | |
// to an array of chapter json promises | |
return story.chapterUrls.map(getJson).reduce(function(chain, chapterPromise) { | |
// Use reduce to chain the promises together, | |
// but adding content to the page for each chapter | |
return chain.then(function() { | |
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'; | |
}); |
// With Node.js-style callbacks | |
// A couple of functions to save repetiton | |
function handleError(err) { | |
addTextToPage("Argh, broken: " + err.message); | |
hideSpinner(); | |
} | |
function hideSpinner() { | |
document.querySelector('.spinner').style.display = 'none'; | |
} | |
getJsonCallback('story.json', function(err, story) { | |
// In Node.js callbacks, the first param is an error | |
if (err) { | |
handleError(err); | |
return; | |
} | |
addHtmlToPage(story.heading); | |
// We need to track if anything failed | |
var errored = false; | |
// Track the next chapter that can appear on the page | |
var nextChapterToAdd = 0; | |
var chapterHtmls = []; | |
story.chapterUrls.forEach(function(chapterUrl, i) { | |
getJsonCallback(chapterUrl, function(err, chapter) { | |
// If we've already errored, don't want to do anything | |
if (errored) { | |
return; | |
} | |
// If this is an error, handle it | |
if (err) { | |
errored = true; | |
handleError(err); | |
return; | |
} | |
// Store this chapter, we might not be able to deal with it yet | |
chapterHtmls[i] = chapter.html; | |
// Try and add new chapters to the page, in order. | |
// Stop when we reach a chapter we've yet to download. | |
for (; nextChapterToAdd in chapterHtmls; nextChapterToAdd++) { | |
addHtmlToPage(chapterHtmls[nextChapterToAdd]); | |
// Did we just add the final chapter? | |
if (nextChapterToAdd == story.chapterUrls.length - 1) { | |
addTextToPage("All done"); | |
hideSpinner(); | |
} | |
} | |
}); | |
}); | |
}); |
// With events (using simpler on* properties) | |
// A couple of functions to save repetiton | |
function handleError(err) { | |
addTextToPage("Argh, broken: " + err.message); | |
hideSpinner(); | |
} | |
function hideSpinner() { | |
document.querySelector('.spinner').style.display = 'none'; | |
} | |
// In this example, we get an object we add load & error listeners to | |
var request = getJsonEvent('story.json'); | |
request.onload = function() { | |
addHtmlToPage(request.response.heading); | |
// We need to track if anything failed | |
var errored = false; | |
// Track the next chapter that can appear on the page | |
var nextChapterToAdd = 0; | |
var chapterHtmls = []; | |
// We store chapter requests so we can remove listeners later | |
var chapterRequests = request.response.chapterUrls.map(function(chapterUrl, i) { | |
var request = getJsonEvent(chapterUrl); | |
request.onload = function() { | |
// Store this chapter, we might not be able to deal with it yet | |
chapterHtmls[i] = request.response.html; | |
// Try and add new chapters to the page, in order. | |
// Stop when we reach a chapter we've yet to download. | |
for (; nextChapterToAdd in chapterHtmls; nextChapterToAdd++) { | |
addHtmlToPage(chapterHtmls[nextChapterToAdd]); | |
// Did we just add the final chapter? | |
if (nextChapterToAdd == story.chapterUrls.length - 1) { | |
addTextToPage("All done"); | |
hideSpinner(); | |
} | |
} | |
}; | |
request.onerror = function(event) { | |
// prevent further events | |
chapterRequests.forEach(function(request) { | |
request.onload = null; | |
request.onerror = null; | |
}); | |
handleError(event.error); | |
}; | |
return request; | |
}); | |
}; | |
request.onerror = function(event) { | |
handleError(event.error); | |
}; |
// With Promises + Generators (and the spawn helper) | |
spawn(function *() { | |
try { | |
// 'yield' effectively does an async wait, returning the result of the promise | |
let story = yield getJson('story.json'); | |
addHtmlToPage(story.heading); | |
// Map our array of chapter urls | |
// to an array of chapter json promises. | |
// This makes sure they all download parallel. | |
let chapterPromises = story.chapterUrls.map(getJson); | |
// Can't use chapterPromises.forEach, because yielding within doesn't work | |
for (let chapterPromise of chapterPromises) { | |
// Wait for each chapter to be ready, then add it to the page | |
let chapter = yield chapterPromise; | |
addHtmlToPage(chapter.html); | |
} | |
addTextToPage("All done"); | |
} | |
catch (err) { | |
// try/catch just works, rejected promises are thrown here | |
addTextToPage("Argh, broken: " + err.message); | |
} | |
document.querySelector('.spinner').style.display = 'none'; | |
}); |
This comment has been minimized.
This comment has been minimized.
@WebReflection @jakearchibald pretty simple in RxJS as well, for example: /* Assume getJSON is an Observable, else a Promise wrapped as Observable using
Rx.Observable.fromPromise */
var observable = getJSON('story.json')
.do(function (story) { addHtmlToPage(story.heading); })
.flatMap(function (story) {
return Rx.Observable.for(story.chapters, getJSON);
});
var subscription = observable.subscribe(
function (chapter) {
addHtmlToPage(chapter.html);
},
function (err) {
addTextToPage("Argh, broken: " + err.message);
document.querySelector('.spinner').style.display = 'none';
},
function () {
addTextToPage("All done");
document.querySelector('.spinner').style.display = 'none';
}); |
This comment has been minimized.
This comment has been minimized.
just in case somebody else would like to "play" … the game here is the ability to render chapter 1, 2, and 3 at once then chapter 4 and 5 in a second "flush" considering a disordered reply chapters order such: 2, 5, 3, 1, 4 where 5 won't be rendered but 1,2,3 will after the 1 is complete, and 4,5 will be rendered after the 4 is complete. Anything that just render all of them at once is not valid example Accordingly, check your code before posting and show your magic ;-) |
This comment has been minimized.
This comment has been minimized.
Why does the generator example have try/catch? |
This comment has been minimized.
This comment has been minimized.
@rwaldron to catch errors :D Rejected promises are thrown as errors. There's a section on this at http://www.html5rocks.com/en/tutorials/es6/promises/ |
This comment has been minimized.
This comment has been minimized.
@WebReflection love the technique of reusing the original array! However, you should include the handleError definition, and you need to stop the spinner on error/success. Also, unless I've read it wrong, your error handler may get called multiple times if multiple chapters fail? You'll need something to prevent that. |
This comment has been minimized.
This comment has been minimized.
@mattpodwysocki that's really nice. The plain promises example loses out because there's a lack of "map" or similar, which is a shame because I don't think the reduce stuff is awfully friendly. Is your error message going to be displayed once at most, even if multiple chapters fail? Also, you need to hide the spinner on error/success. |
This comment has been minimized.
This comment has been minimized.
If we're going for brevity, here's the generators example: // getJSON returns an ES6 Promise, spawn implementation is at
// http://www.html5rocks.com/en/tutorials/es6/promises/
spawn(function *() {
try {
let story = yield getJson('story.json');
addHtmlToPage(story.heading);
story.chapterUrls.map(getJson).forEach(p => addHtmlToPage((yield p).html));
addTextToPage("All done");
} catch (err) { addTextToPage("Argh, broken: " + err.message); }
document.querySelector('.spinner').style.display = 'none';
}); |
This comment has been minimized.
This comment has been minimized.
Just to expand on the rules laid down by @WebReflection:
|
This comment has been minimized.
This comment has been minimized.
@jakearchibald the error and finalization are only called at a maximum once. The nice thing is that we can add a |
This comment has been minimized.
This comment has been minimized.
updated accordingly with rules … well, it might be more beautiful despite the amount of lines … very readable, much wow :D |
This comment has been minimized.
This comment has been minimized.
Nice, fun exercise. I enjoyed the challenge. :) I'm including here my two answers, using asynquence, which is a 1.1k minzipped abstraction on top of promises. BTW, I didn't go for least-number-of-LoC, I wanted most-readable form, so I factored out many of the inline anonymous functions to named functions. The boiler-plate: function getJson(url) {
return ASQ(function(done){
// using jquery $.get(..)
$.get(url).success(done).error(done.fail);
});
}
function allDone() {
addTextToPage("All done");
document.querySelector('.spinner').style.display = 'none';
}
function loadingError(err) {
// catch any error that happened along the way
addTextToPage("Argh, broken: " + err.message);
document.querySelector('.spinner').style.display = 'none';
} The first solution uses asynquence almost identically to the first promises example above. While some don't like getJson("story.json")
.seq(function(story){
function waitForChapter(sq,loadingChapter) {
return sq
.seq(loadingChapter)
.val(renderChapter);
}
function renderChapter(chapterJson) {
addHtmlToPage(chapterJson.html);
}
addHtmlToPage(story.heading);
return story.chapterUrls
.map(getJson)
.reduce(waitForChapter,ASQ()); // ASQ() creates an empty sequence
})
.val(allDone)
.or(loadingError); The second solution sets up both an iterable getJson("story.json")
.seq(function(story){
function waitForChapter(sq,loadingChapter) {
return sq
.seq(loadingChapter)
.val(rendering.next);
}
function renderChapter(chapterJson) {
addHtmlToPage(chapterJson.html);
}
addHtmlToPage(story.heading);
// iterable `rendering` sequence, one step for each chapter rendering
// ASQ.iterable(..) creates an empty iterable sequence
var rendering = ASQ.iterable.apply(null,
story.chapterUrls
.map(function(){ return renderChapter; })
);
// sequence for loading chapters and interleaving iteration control of `rendering` sequence
story.chapterUrls
.map(getJson)
.reduce(waitForChapter,ASQ()) // ASQ() creates an empty sequence
.or(rendering.throw);
return rendering;
})
.val(allDone)
.or(loadingError);
Update: Iterable Sequences are now in asynquence, and work as shown above! |
This comment has been minimized.
This comment has been minimized.
Update: Realized another way to do the promises-style example, without needing reduce, but instead another getJson("story.json")
.seq(function(story){
function waitForChapter(loadingChapter) {
return function() {
return loadingChapter
.val(renderChapter);
};
}
function renderChapter(chapterJson) {
addHtmlToPage(chapterJson.html);
}
addHtmlToPage(story.heading);
return ASQ()
.seq.apply(null,
story.chapterUrls
.map(getJson)
.map(waitForChapter)
);
})
.val(allDone)
.or(loadingError); Here's how it works:
The end-result of this map/map/apply trick is basically the same as: // fire off the loading of each chapter, all in parallel
var chapter_loading = [
getJson(story.chapterUrls[0]), // start loading chapter 1 right now
getJson(story.chapterUrls[1]), // start loading chapter 2 right now
getJson(story.chapterUrls[2]), // start loading chapter 3 right now
getJson(story.chapterUrls[3]), // start loading chapter 4 right now
getJson(story.chapterUrls[4]) // start loading chapter 5 right now
];
ASQ()
// make sure chapter 1 has loaded, wait if not yet...
.seq(chapter_loading[0])
// now render chapter 1
.val(renderChapter)
// once chapter 1 finishes rendering, then proceed
// make sure chapter 2 has loaded, wait if not yet...
.seq(chapter_loading[1])
// now render chapter 2
.val(renderChapter)
// once chapter 2 finishes rendering, then proceed
// make sure chapter 3 has loaded, wait if not yet...
.seq(chapter_loading[2])
// now render chapter 3
.val(renderChapter)
// once chapter 3 finishes rendering, then proceed
// make sure chapter 4 has loaded, wait if not yet...
.seq(chapter_loading[3])
// now render chapter 4
.val(renderChapter)
// once chapter 4 finishes rendering, then proceed
// make sure chapter 5 has loaded, wait if not yet...
.seq(chapter_loading[4])
// now render chapter 5
.val(renderChapter)
// once chapter 5 finishes rendering, then proceed
// all chapters rendered!
.val(allDone)
// oops, something went wrong along the way :(
.or(loadingError); |
This comment has been minimized.
I don't know Jake … you almost convinced me then I've read the comparison by number of lines and spot many differences, declared functions, empty lines, etc etc … so here the eddy.js (a 2.2KB minzipped utility) version of your first Promises based example, still using events and bits of some smart trick you put in there.