MR WHY

I am
Wang Hongyang

Wechat Official Account
Find fun things here!

JavaScript Promises Study Notes

This article is the Udacity online course notes of JavaScript Promises by Google. The teacher of this course is Cameron Pittman.

Callbacks vs Thens

"The Promise object is used for deferred and asynchronous computations." — MDN

What is asynchronous work?

Asynchronous work happens at an unknown or unpredictable time. Normally code is synchronous: one statement executes, there is a guarantee that the next statement executes immediately afterwards. This is ensured by the JavaScript threading model that for all intents and purposes, JavaScript runs in a single timeline. For example:

// Only one timeline to run these two lines of code, one after another.
var planetName = "Kepler 22 b";
console.log(planetName); // Kepler 22 b

Asynchronous code is not guaranteed to execute in a single unbroken timeline. The complete time of asynchronous operations is not predictable. For example:

// Even the first request is sent first, we can not assume the first one will be full-filled first.
var file1 = get('file1.json');
var file2 = get('file2.json');
console.log(file1); // undefined
console.log(file2); // undefined

So what is the best way to handle asynchronous code?

function loadImage(src, parent, callback) {
  var img = document.createElement('img');
  img.src = src;
  img.onload = callback;
  parent.appendChild(img);
};

Callbacks are the default JavaScript technique for handling asynchronous work. Pass the callback function to another function and then call the callback function at some later time when some conditins have been met.

How do you handle errors?

Error handling needs to be considered since there is chance to fail because of network, server no respond, wrong JSON format, and so on.

How do you create a sequence of work?

There is one scenario that leads to something called the Pyramid of Doom/Callback Hells.

loadImage('above-the-fold.jpg', imgContainer, function(){
  loadImage('just-below-the-fold.jpg', imgContainer2, function(){
    loadImage('farther-down.jpg', imgContainer3, function(){
      loadImage('this-is-getting-ridiculous.jpg', imgContainer4, function(){
        loadImage('abstract-art.jpg', imgContainer5, function(){
          loadImage('egyptian-pyramids.jpg', imgContainer6, function(){
            loadImage('last-one.jpg', imgContainer7);
          }
        }
      }
    }
  }
})

What's the problem with this way of writing code?

It's hard to write. It looks ugly. And most important: it's incredibly frustrating to debug.

Let's try another way of writing this.

var sequence = get('example.json')
  .then(doSomething)
  .then(doSomethingElse);

Writing with Promise makes codes easy to read and understand.

Four States of Promise

Fulfilled (Resolved)

It worked. :)

Rejected

It didn't work. :(

Pending

Still waiting...

Settled

Something happened! Settled means that the promise has either fulfilled or rejected.

Promise Timeline

For the left side of setting event listener after event fires: When we set the event listener after event fires, nothing will happen.

For the right side of Pormise: Even the action is set after the Promise has been resolved, it will execute.

Another difference between these two methods is that event listener can be fired many times, but a Promise can be settled only once. For example:

new Promise(function(resolve, reject) {
  resolve('hi');  // works
  resolve('bye'); // cannot happen a second time
})

Note that Promises execute in the main thread, which means that they are still potentially blocking:

If the work that happens inside the promise takes a long time, there's still a chance that it could block the work the browser needs to do to render the page.

Promise Syntax

new Promise(function(resolve[, reject]) {
  var value = doSomething();
  if(thingWorked) {
    resolve(value); // #1
  } else if (somethingWentWrong) {
    reject();
  }
}).then(function(value) { // #2
  // success!
  return nextThing(value);
}).catch(rejectFunction);

// Note that "value" at #1 is the same with #2

Promise in ES2015:

new Promise((resolve, reject) => {
  let value = doSomething();
  if(thingWorked) {
    resolve(value);
  } else if (somethingWentWrong) {
    reject();
  }
}).then(value => nextThing(value))
.catch(rejectFunction);

Notation Promise is a constructor. A promise can either be stored as a variable var promise = new Promise(); or simply work on it as soon as create it like the code block above.

Note that resolve and reject have the same syntax. resolve leads to the next then in the chain and reject leads to the next catch.

Incidentally, if there is a JavaScript error somewhere in the body of the promise, .catch will also automatically get called.

An example of utilizing Promise to load image:

new Promise(function(resolve, reject) {
  var img = document.createElement('img');
  img.src = 'image.jpg';
  img.onload = resolve;
  img.onerror = reject;
  document.body.appendChild(img);
})
.then(finishLoading)
.catch(showAlternateImage);

What's this in Promise?

This question is very tricky: this in Promise is different based on the JavaScript stansard you are using.

ES5

var someObj = function(){};

someObj.prototype.PromiseMethod = function(ms) {
  return new Promise(function(resolve) {
    console.log(this); // 1
    setTimeout(function() {resolve();}, ms);
  });
}

var instance = new someObj();
instance.PromiseMethod(3000).then(function(){console.log("done")});

The this in Promise written by ES5 is the global Object, or say the window.

ES2015 (ES6)

var someObj = function(){};

someObj.prototype.PromiseMethod = function(ms) {
  return new Promise(resolve => {
    console.log(this); // 2
    setTimeout(function() {resolve();}, ms);
  });
}

var instance = new someObj();
instance.PromiseMethod(3000).then(function(){console.log("done")});

The this here in Promise written by ES6 is the object someObj, because ES6's this will save the context in asynchronous operations. And this works with callback, too.

Note that this is not special for Promise, it's a special definition in ES6.

Fetch API

Fecth API uses native promises to simplify xml http requests. Here are two utility functions for fetch get and post:

function fetchGet(url, params) {
  return fetch(url).then((res) => {
    if (!res.ok) {
      throw Error(res.statusText);
    }
    return res.json();
  });
}

function fetchPost(url, params) {
  console.log(JSON.stringify(params));
  return fetch(url, {
    method: 'POST',
    body: JSON.stringify(params),
    headers: {
      'Content-Type': 'application/json'
    }
  })
    .then(res => res.json());
}

Error Handling

Remember that .catch is just shorthand for .then(undefined, rejectFunc):

// the following two blocks are equal
get('example.json')
  .then(resolveFunc)
  .catch(rejectFunc);

get('example.json')
  .then(resolveFunc)
  .then(undefined, rejectFunc);

The full function signature for then is actually this:

get('example.json')
  .then(resolveFunc, rejectFunc); // resolveFunc and rejectFunc cannot be called both.

But note that in the full function signature, resolveFun and rejectFunc cannot be called both. Yet in the former signature of seperating then and catch, they can be called both.

In all cases, as soon as a promise rejects, then the JavaScript engine skips to the next reject function in the chain, whether that's in a .catch or a .then. For example:

get('example.json') // #A
  .then(data => {
    updateView(data); // #B
    return get(data.anotherUrl);
  })
  .catch(error => { // 1
    console.log(error);
    return recoverFromError();
  })
  .then(doSomethingAsync)
  .then(data => soSomethingElseAsync(data.property), 
        error => { // 2
          console.log(error);
          tellUserSomethingWentWrong();
        })

An error happening at #A or #B will both be caught by 1 and 2 (2 is also rejectFunc as said before).

Here is another more complex error handling example, fill what numbers will be logged if errors occur on lines #A, #B, #C, and #D, and only these lines?

var urls = [];
async('example.json') // #A ----------> [?]
  .then(function(data) {
    urls = data.urls; // #B ----------> [?]
    return async(urls[0]);
  })
  .then(undefined, function(e) {
    console.log(1);
    return recovery();
  })
  .catch(function(e) {
    console.log(2);
    return recovery(); // #C ----------> [?]
  })
  .then(function() {
    console.log(3);
    return async(urls[1]); // #D ------> [?]
  })
  .then(async, function(e) {
    console.log(4);
    ahhhIGiveUp();
  });

Answer:

  • Error at #A —> 1, 3: the first rejectFunc will catch the error and log 1, then recovery and continue to execute the next then, which logs 3;
  • Error at #B —> 1, 3: the same situation with #A;
  • Error at #C —> none: this is an interesting one, because the recovery function is in a rejectFunc, it is only going to be called only if another error happened before. But we ruled only one error can happen, so only #C error is an impossible case.
  • Error at #D —> 4: the next reject function will get called.

Chaining

Asynchronous work may not be isolated, the next Promise may ask for the value from previous Promise to get executed properly, this is called chaining here.

There are two main startegies for performing multiple asynchronous actions: actions in series and actions in parallel.

  • Actions in series: occur one after another;
  • Actions in parallel: occur simultaneously.

This is an example contains both actions in series and actions in parallel:

getJSON('thumbsUrl.json')
  .then(function(response) {
    response.results.forEach(function(url) {
      getJSON(url).then(createThumb);
    });
  });

In this example, 'thumbsUrl.json' is got first, then looping the url list to get thumbnails in parallel. One issue is that the thumbnails will be created in a random order. The timeline looks like this:

To keep the thumbnails in original order, we can wrap the getJSON in the chain .then so that the next getJSON will not be executed until the thum has been created:

getJSON('thumbsUrl.json')
  .then(function(response) {
    var sequence = Promise.resolve();
  
    response.results.forEach(function(url) {
      sequence = sequence.then(function() {
        return getJSON(url);
      })
        .then(createThumb);
    });
  });

The good news is: all thumbnails are in order. The bad news is: the cost time is much longer as shown below:

So how can we keep the order as well as chain actions in parallel instead of series? We can combine .map method which output an array with Promise.all which takes in an array of promises, executes them, and returns an array of values in the same order:

getJSON('thumbsUrl.json')
  .then(function(response) {
    var arrayOfPromises = response.results.map(function(url) {
      getJSON(url);
    });
    return Promise.all(arrayOfPromises);
  })
  .then(function(arrayOfPlaneData) {
    arrayOfPlaneData.forEach(function(planet) {
      createPlanetThum(planet);
    });
  })
  .catch(function(error) {
    console.log(error);
  });

Note that Promise.all fails quick. Once a certain element in the array fails, the process will be interrupted. The timeline of this block of code is:

Finally, I recommend this amazing article — We have a problem with promises by Nolan Lawson. I got a much deeper understanding in Promises from his blog post.

310

分享本文:

TOC