JavaScript Promises 学习笔记

本文是 ECMAScript 2015 原生异步方法 Promise 的学习笔记。网上课程由 Udacity + Google 提供,老师是卡梅伦·皮特曼(Cameron Pittman)。

学习笔记分为 8 个部分:

  1. callbacks vs thens
  2. Promise 中的 4 个状态
  • Fulfilled (Resolved)
  • Rejected
  • Pending
  • Settled
  1. Promise 时间线
  2. Promise 语法
  3. Promise 中的 this
  4. Fetch API
  5. 错误处理
  6. Chaining

以下是全文:

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 .cath 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:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容

  • **2014真题Directions:Read the following text. Choose the be...
    又是夜半惊坐起阅读 8,595评论 0 23
  • 昨晚,熙茹因为外公外婆晚饭后要返回舅舅家而感到心情不好,嘴里嘟囔着自家的不好,熙爸就坐在沙发上看着她呵呵笑,她就越...
    端木沐溪阅读 256评论 0 0
  • 站在风的尽头 我唱清歌 流水中有花瓣飘过 十月的河流有些凉薄 带走了一些甜的过往 在水边 我独舞 水珠溅到冰凉的睫...
    云中飘舞阅读 433评论 10 31
  • 如何获得获得人人羡慕的好运气?我觉得运气是跟努力,用心相关联的。 1、开启大脑的准备状态:做个有心之人,时刻关注寻...
    石话石说简书阅读 239评论 0 1
  • huihui1阅读 147评论 0 2