Skip to content

Instantly share code, notes, and snippets.

@jakearchibald
Last active July 8, 2022 19:24
Show Gist options
  • Star 66 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save jakearchibald/0e652d95c07442f205ce to your computer and use it in GitHub Desktop.
Save jakearchibald/0e652d95c07442f205ce to your computer and use it in GitHub Desktop.
Getting some urls for content, then fetching that content & adding it to the page sequentially. Promises vs node style callbacks vs events.
// 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';
});
@getify
Copy link

getify commented Dec 30, 2013

Update:

Realized another way to do the promises-style example, without needing reduce, but instead another map(..) call, plus apply(..). This feels a bit more readable IMO:

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:

  1. getJson("story.json") fires off a request to get the JSON for the story.
  2. Once the JSON request finishes, seq(function(story){... runs, where story is the JSON object.
  3. addHtmlToPage(story.heading) renders the story heading to the page.
  4. story.chapterUrls is an array of strings (URLs).
  5. story.chapterUrls.map(getJson) calls getJson() for each URL string in the original array, which produces a new array of loading sequences, one for each chapter URL.
  6. story.chapterUrls.map(getJson).map(waitForChapter) calls waitForChapter() for each loading-chapter sequence, which returns a function that will generate a new sequence which listens for each chapter loading sequence to finish and then calls renderChapter() to render that newly loaded chapter. The result of this second map(..) call is yet another new array, a series of functions which, when called, each produce a load-and-render sequence for each chapter.
  7. ASQ().seq.apply(null, ... ) takes this array of functions and applys it to ASQ.seq(), which chains each chapter load-and-render sequence as sequential steps in this main sequence. So each chapter sequence will, when the previous chapter finishes rendering, check to see if the next chapter is loaded, and if so, render the it, otherwise will keep waiting.

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);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment