Quantcast
Channel: 懒得折腾
Viewing all articles
Browse latest Browse all 764

Introduction to Promises

$
0
0

Introduction to Promises

This guide assumes familiarity with basic JavaScript and should be suitable for both people new to asynchronous programming and those with some experience.

Motivation

We want our code to be asynchronous, because if we write synchronous code then the user interface will lock up (in client side applications) or no requests will get handled (in server applications). One way to solve this problem is threads, but they create their own problems and are not supported in JavaScript.

One of the simplest ways to make functions asynchronous is to accept a callback function. This is what node.js does (at time of writing). This works, but has a number of issues.

  1. You lose the separation of inputs and outputs to a function since the callback must be passed as an input
  2. It is difficult to compose multiple serial or parallel operations
  3. You lose a lot of helpful debugging information and error handling ability relating to stack traces and the bubbling up of exceptions
  4. You can no longer use the built in control flow constructs and they must all be re-invented to work asynchronously.

Many APIs in the browser use some kind of event based model for control flow, which solves problem 1, but not problems 2 to 4.

Promises aim to solve issues 1 to 3 and can solve problem 4 in ES6 (with the use of generators).

Basic Usage

The core idea behind promises is that a promise represents a value that is the result of an asynchronous operation. They may instead turn out to be a thrown error. Asynchronous functions should return promises:

var prom = get('http://www.example.com')

If we request the content of the web page http://www.example.com we will be doing it asynchronously so we get a promise back.

In order to extract the value from that promise, we use .done which queues a function to be executed when the promise is fulfilled with some result.

var prom = get('http://www.example.com')
prom.done(function (content) {
  console.log(content)
})

Note how we’re passing a function that has not been called to .done and it will be called only once, when the promise is fulfilled. We can call .done as many times as we want and as late or early as we want and we will always get the same result. For example, it’s fine to call it after the promise has already been resolved:

var cache = {}
function getCache(url) {
  if (cache[url]) return cache[url]
  else return cache[url] = get(url)
}

var promA = getCache('http://www.example.com')
promA.done(function (content) {
  console.log(content)
})
setTimeout(function () {
  var promB = getCache('http://www.example.com')
  promB.done(function (content) {
    console.log(content)
  })
}, 10000)

Of course, requesting an error page can easilly go wrong, and throw an error. By default, .done just throws that error so it gets logged appropriately and (in environments other than the browser) crashes the application. We often want to attach our own handler instead though:

var prom = get('http://www.example.com')
prom.done(function (content) {
  console.log(content)
}, function (ex) {
  console.error('Requesting www.example.com failed, maybe you should try again?')
  console.error(ex.stack)
})

Transformation

Often you have a promise for one thing and you need to do some work on it to get a promise for another thing. Promises have a .thenmethod that works a bit like .map on an array.

function getJSON(url) {
  return get(url)
    .then(function (res) {
      return JSON.parse(res)
    })
}

getJSON('http://www.example.com/foo.json').done(function (res) {
  console.dir(res)
})

Note how .then handles any errors for us so that they bubble up the stack just like in synchronous code. You can also handle them when you call .then

function getJSON(url) {
  return get(url)
    .then(function (res) {
      return JSON.parse(res)
    }, function (err) {
      if (canRetry(err)) return getJSON(url)
      else throw err
    })
}

getJSON('http://www.example.com/foo.json').done(function (res) {
  console.dir(res)
})

Here, errors thrown by JSON.parse are not handled by the error handler we attached, but some errors we received from calling get are handled with a retry. Note how we can return a promise from .then and it is automatically unwrapped:

var prom = get('http://example.com/url-to-request')
  .then(function (url) {
    return get(url)
  })
  .then(function (res) {
    return JSON.parse(res)
  })
prom.done(function (finalResult) {
  console.dir(finalResult)
  //this is actually the very final result
})

Combination

One advantage of a promise being a value is that you can perform useful operations to combine promises. One such operation that most libraries support is all:

var a = get('http://www.example.com')
var b = get('http://www.example.co.uk')
var both = Promise.all([a, b])
both.done(function (res) {
  var a = res[0]
  var b = res[1]
  console.dir({
    '.com': a,
    '.co.uk': b
  })
})

This is extremely useful if you need to run lots of operations in parallel. The idea also extends to large, unbounded arrays of values:

function readFiles(files) {
  return Promise.all(files.map(function (name) {
    return readFile(name)
  }))
}
readFiles(['fileA.txt', 'fileB.txt', 'fileC.txt']).done(function (filesContents) {
  console.dir(filesContents)
})

Of course, serial operations can be composed just using .then

get('http://www.example.com').then(function (res) {
  console.log('.com')
  console.dir(res)
  return get('http://www.example.co.uk')
}).done(function (res) {
  console.log('.co.uk')
  console.dir(res)
})

And with a little imagination you can use this technique to handle arrays as well:

function readFiles(files) {
  var result = []

  // create an initial promise that is already fulfilled with null
  var ready = Promise.from(null)

  files.forEach(function (name) {
    ready = ready.then(function () {
      return readFile(name)
    }).then(function (content) {
      result.push(content)
    })
  })

  return ready.then(function () {
    return result
  })
}
readFiles(['fileA.txt', 'fileB.txt', 'fileC.txt']).done(function (filesContents) {
  console.dir(filesContents)
})

Implementations / Downloads

There are a large number of Promises/A+ compatible implementations out there, not all of which have .done methods or Promise.allmethods. You should feel free to use whichever implementation best fits in with your needs. Here are the two I would recommend.

Promise

Promise, by Forbes Lindesay, is a very simple, high performance promise library. It is designed to just provide the bare bones required to use promises in the wild.

If you use node.js or browserify you can install it using npm:

npm install promise

and then load it using require:

var Promise = require('promise')

If you are using any other module system or just want it directly in the browser, you can download a version with a standalone module defenition from here (with UMD support) or add a script tag directly:

<script src="http://www.promisejs.org/implementations/promise/promise-3.2.0.js"></script>

Once installed, you can create a new promise using:

var myPromise = new Promise(function (resolve, reject) {
  // call resolve(value) to fulfill the promise with that value
  // call reject(error) if something goes wrong
})

Full documentation can be found at https://github.com/then/promise

Q

Q, by Kris Kowal, is an advanced, fully featured promise library. It is designed to be fully featured and has lots of helper methods to make certain common tasks easier. It is somewhat slower than Promise, but can make up for this with support for better stack traces and additional features.

If you use node.js or browserify you can install it using npm:

npm install q

and then load it using require:

var Q = require('q')

If you are using any other module system or just want it directly in the browser, you can download a version with a standalone module defenition from here (with UMD support) or add a script tag directly:

<script src="http://www.promisejs.org/implementations/q/q-0.9.6.js"></script>

Once installed, you can create a new promise using:

var myPromise = Q.promise(function (resolve, reject) {
  // call resolve(value) to fulfill the promise with that value
  // call reject(error) if something goes wrong
})

Full documentation can be found at https://github.com/kriskowal/q

Other

You can find more implementations here



Viewing all articles
Browse latest Browse all 764

Trending Articles