Last active
January 29, 2018 02:35
-
-
Save faceyspacey/587f1f478ac94a9eb8e20b61202be09c to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const job1 = cb => setTimeout(() => cb('first'), 900) | |
const job2 = cb => setTimeout(() => cb('second'), 100) | |
const job3 = cb => setTimeout(() => cb('third'), 300) | |
const jobs = [job1, job2, job3] | |
const asyncMap = (jobs, callback) => { | |
const jobPromises = jobs.map(job => { // loop through jobs | |
return new Promise((resolve, reject) => { // return a promise for each item in the array | |
return job(resolve) // resolve each promise to the value passed to cb() | |
}) | |
}) | |
// use Promise.all to evaluate them concurrently/asynchronously as requested (it guarantees initial order) | |
Promise.all(jobPromises).then(results => { | |
callback(results) // results is guaranteed to be in order by virtue of the way Promise.all works | |
}) | |
} | |
asyncMap(jobs, (results) => { | |
console.log(results) | |
}) | |
// here's a shorter way to write this: | |
const asyncMap2 = (jobs, callback) => { | |
const jobPromises = jobs.map(job => new Promise((res, rej) => job(res))) | |
Promise.all(jobPromises).then(callback) | |
} | |
// lastly, to make things simply in your mind, imagine that `cb` is called `resolve` in each of the jobs instead: | |
const j1 = resolve => setTimeout(() => resolve('first'), 900) | |
const j2 = resolve => setTimeout(() => resolve('second'), 100) | |
const j3 = resolve => setTimeout(() => resolve('third'), 300) | |
// so `resolve` is simply the resolve function from `new Promise((resolve, reject) => { ...` | |
// The main thing to understand is simply how `new Promise` works. It works by passing it a function. Right. | |
// That function is then called with 2 args: `resolve` and `reject`. Forget about `reject` for now. It's the same | |
// as `resolve` except used for failures. | |
// | |
// So you got `resolve` you can now call it any time you want, such as after a timeout. When it does, then | |
// the value "returned" from the promise will be the value passed to `resolve`. I say "returned" in quotes | |
// because you use promise style syntax with `.then` to access it: | |
// | |
// new Promise(func).then(result => console.log(result)) | |
// | |
// It boils down to just understanding those mechanics. It can be a bit tricky. I'll say it a bland but succinct way: | |
// | |
// To create a promise, you pass `new Promise` a function, which will be called internally and passed a `resolve` | |
// argument. `resolve` is a function that takes one argument. When you then call it with an argument, that value you pass | |
// will be accessbile in `.then(results => ...`, which can execute asynchrously at a later point in time. I.e. `.then()` | |
// won't execute immediately as the next line will. At the very least, it will execute in the next "tick", which is the next | |
// time the javascript engine tries to evaluate pending code. But more than likely, it will execute a lot later when a timer | |
// or async data fetching job finishes. The end. | |
// | |
// Promise.all (a built in function) handles all the work of making sure results match up etc. | |
// AND SUPER LASTlY, in case they want to see it by hand without the automation `Promise.all` provides, here it is: | |
const asyncMapCustom = (jobs, callback) => { | |
const resultsHash = {} | |
const total = jobs.length | |
let completed = 0 | |
jobs.forEach((job, index) => { | |
new Promise((resolve, reject) => { | |
job(value => { | |
// now instead of passing just the value to `resolve`, we pass, the index (which we have from the enclosing grand parent function) | |
// and most importantly the `done` key which will only be true once the last promise resolves (by virtue of tracking how many | |
// are completed and when that number equals the total jobs). | |
// NOTE: the key difference between this and `job(resolve)` above is that we write our own intermediary function to intercept | |
// the results, do some calculations (i.e. done + index), and then past that object to `resolve`, which we can destructure below when each are complete | |
resolve({ value, index, done: ++completed === total }) | |
}) | |
}).then(({ value, index, done }) => { // as we loop through each and create the above promise, we immediately call `.then` on it | |
resultsHash[index] = value // will create a hash that looks like this: { '1': 'second', '2': 'third', '0': 'first' } | |
if (done) { | |
const orderedKeys = Object.keys(resultsHash).sort() // will create an array like this: [0, 1, 2] | |
const results = orderedKeys.map(index => resultsHash[index]) // now we can key into the hash, getting the value in order | |
callback(results) // log the results | |
} | |
}) | |
}) | |
} | |
// also note: notice we didn't have to do any returns here, i.e. `return new Promise`, or even assign `jobs.map` to | |
// `jobPromises`. We use `forEach` instead since, the callback is called when any single one of the promises decides | |
// it's done. | |
asyncMapCustom(jobs, (results) => { | |
console.log(results) | |
}) | |
// FINAL NOTE: all this has been tested and works. Paste it into the Chrome console. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
And here's probably the best version. It doesn't include promises at all, and is 100% custom. I mean "Promise" is built in to
all modern JS implementations, so using that is still considered "low level" but here is the pre-Promise absolute most custom version (it's also simpler to read):