Skip to content

Instantly share code, notes, and snippets.

@faceyspacey
Last active January 29, 2018 02:35
Show Gist options
  • Save faceyspacey/587f1f478ac94a9eb8e20b61202be09c to your computer and use it in GitHub Desktop.
Save faceyspacey/587f1f478ac94a9eb8e20b61202be09c to your computer and use it in GitHub Desktop.
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.
@faceyspacey
Copy link
Author

faceyspacey commented Jan 29, 2018

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

const asyncMapCustomSimplified = (jobs, callback) => {
  const resultsHash = {}
  const total = jobs.length
  let completed = 0

  jobs.forEach((job, index) => {
    job(value => { // as we loop through each job, simply call it with the below callback that does everything in one place :)
      const done = ++completed === total
      resultsHash[index] = value // will create a hash that is potentially out of order like this: { '1': 'second', '2': 'third', '0': 'first' }

      if (done) {
        const orderedKeys = Object.keys(resultsHash).sort() // now we insure the correct order and get 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
      }
    })
  })
}



asyncMapCustomSimplified(jobs, (results) => {
  console.log(results)
})

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