JavaScript ES6 中的Promise

1. Promises/A+规范html

CommonJS之Promises/A规范,经过规范API接口来简化异步编程,使咱们的异步逻辑代码更易理解。jquery

这里就不作更多的啰嗦了,详细请看
官方文档https://promisesaplus.com/
中文翻译http://www.ituring.com.cn/article/66566git

Promises/A+规范的实现有不少:
JQuery的deferred https://github.com/jquery/jquery
bluebird模块https://github.com/petkaantonov/bluebird
q模块https://github.com/kriskowal/q
async模块https://github.com/caolan/asyncgithub

本文主要介绍ES6规范的Promise对象web

2.Promise对象ajax

一个Promise实例对象,表示一次异步操做的封装,异步操做的结果有两种结果:成功或失败。而后根据异步操做的结果采起不一样的操做。而且能够多 个Promise串联起来使用,也就是把异步操做串联起来,组织成并发或者串行工做流等。经过Promise咱们能够把复杂的异步代码看起来更简洁,理解 起来更容易。数据库

图片描述
图片描述
图片描述

2.1生成Promise实例对象编程

Promise对象是一个构造器函数对象,在使用Promise以前,须要先生成一个Promise对象的实例对象。json

let promise = new Promise(function(resolve,  reject) {
          //异步操做
    if (•••) {
               resolve(value); // success
           } else {
               reject(reason); // failure
          }});

须要传入一个固定格式的函数做为参数:function(resolve,  reject) {}。数组

通常状况下,按照这一标准格式去使用,在这个参数函数体中,编写异步操做代码。而后根据异步操做的结果判断,若是成功,则调用resolve,不然 调用reject。能够这样理解:一次异步执行,首先等获得异步执行的结果,而后根据结果决定下一步作什么,这和流程图的步骤相似。

resolve和reject都是内部提供的方法,Promise实例对象是JS引擎自动调用的,因此这两个参数有JS内部提供,直接使用就能够。

(1)resolve(value):将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved)。并把Promise实例对象的value设置为参数value。以后会调用then方法中的onFulfilled。(2) reject(reason):将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected)。并把Promise实例对象的reason设置为参数reason。以后会调用then方法中的onRejected。

小样:Promise生成的实例对象会被系统异步自动调用

生成Promise实例对象后,function(resolve,  reject) {}参数会被异步自动调用,知道这点很重要。
图片描述
图片描述
咱们模拟了一次生成Promise实例对象,根据打印的结果,发现,function(resolve,  reject) {}被自动调用了,可是这不能肯定这就是异步调用的,因为操做的复杂性,就不真实模拟了,记住这个结论:function(resolve,   reject) {}是系统自动异步调用的,具体时间是不肯定的。

咱们使用了setTimeout模拟了一次异步操做,把它封装到一个Promise实例对象中。当异步操做完成,使用resolve把pm状态修改成resolved,而且把pm的值设置”success”。

2.2Promise原型方法

2.2.1 Promise.prototype.then

promise.then(onFulfilled, onRejected)

then方法用来注册Promise实例对象状态从pending变成其余状态后调用的回调函数。onFulfilled和 onRejected都必须为函数。then方法使得异步编程能够实现链式调用。

参数:(1) onFulfilled:可选参数,函数, Promise实例对象从pending变成fulfilled状态,以后会使用Promise实例对象的value调用onFulfilled。例如:在function(resolve,  reject) {}调用 resolve(value)以后。
 (2) onRejected:可选参数,函数,Promise实例对象从pending变成rejected状态,以后会使用Promise实例对象的reason调用onRejected。例如:在function(resolve,  reject) {}调用reject (reason)以后。返回值:then方法会当即返回一个新的Promise实例对象。当onFulfilled和onRejected返回值是Promise实例pm的时候,then返回的Promise实例的状态会由pm的状态决定,详细请参照Promise规范,知道这点很是重要!

小样:then的基本使用
图片描述

2.2.2 catch

promise.catch (rejection) 方法是 promise. then (null, rejection)的别名catch用于指定发生错误时的回调函数。

在Promise实例对象的状态变成fulfilled或者rejected以前,只要发生错误,就会执行这个回调。
参数和返回值和then相似。

图片描述
图片描述
小样:then(onRejected)和catch(onRejected)的异同
图片描述
图片描述
图片描述

抛出的异常不能被 onRejected捕获,却能够被catch捕获。因此最佳实践是:使用catch而不是onRejected,避免有些异常不能被捕捉到。

小样:异常冒泡
图片描述
异常会向下冒泡,直到遇到catch,中间的then会被忽略。

2.3Promise对象的方法

2.3.1 resolve

Promise. resolve([value])

把value转换成Promise。通常用来把一个对象方便转换成Promise实例对象。TJ的CO模块就用到了这个方法。

能够这样理解:
Promise.resolve(value)
// 等价于
new Promise(resolve => resolve(value))

参数:(1)value:可选。能够是任意的JavaScript合法值。返回值:若是value是非Promise实例对象,则返回一个新的Promise实例对象,且它的状态是fulfilled,它的值为value;若是value是一个Promise实例对象,那么返回的也是这个实例对象。

小样:value为非Promise实例对象
图片描述
图片描述
不论是value是通常对象仍是函数对象,都做为Promise实例对象的值。

小样:value为Promise实例对象
图片描述
当value为Promise实例对象的时候,返回的是本身。

2.3.2 reject

Promise. reject ([reason])

reject和resolve是相似的,请参照resolve作类推。

2.3.3 all  

Promise.All(promisesIterator)

用于将多个Promise实例,包装成一个新的Promise实例。

参数:一个包含Promise实例对象的迭代器。例如数组[p1, p2, …]返回值:一个新的Promise实例对象pm。

*pm状态由参数中的全部Promise实例对象的状态决定:

(1)当参数中全部的Promise实例对象的状态为fulfilled的时候,pm的状态为fulfilled。全部参数Promise实例对象的返回值组成一个数组,做为pm的值。(2)当参数中的Promise实例对象有一个的状态为rejected的时候,它的状态为rejected。此时第一个被reject的Promise实例对象的返回值做为pm的reason。

小样:全部参数的Promise实例对象的状态都为fulfilled

图片描述
图片描述
小样:有一个参数的Promise实例对象的状态为rejected
图片描述
图片描述

2.3.4 race

Promise.race(promisesIterator)

用于将多个Promise实例,包装成一个新的Promise实例。

参数:一个包含Promise实例对象的迭代器。例如数组[p1, p2, …]返回值:一个新的Promise实例对象pm。*pm状态由参数中的第一个改变状态的Promise实例对象的状态决定(正如它的名字同样,race-比赛,胜者为王!):(1)若是第一个改变的状态为fulfilled,那么pm的状态为fulfilled,而且pm的值为第一个改变状态的Promise实例对象的值。(2)若是第一个改变的状态为rejected,那么pm的状态为reject,而且pm的reason为第一个状态改变的Promise实例对象的值。

小样:第一个改变的状态为fulfilled

图片描述

使用setTimeout函数,让pm2比pm1延迟一秒修改状态。pm1在一秒后状态变为fulfilled,此时pms的状态也跟着变为fulfilled,已经不须要考虑pm2的执行。

小样:第一个改变的状态为rejected

图片描述
图片描述

使用setTimeout函数,让pm1比pm2延迟一秒修改状态。pm2在一秒后状态变为rejected,此时pms的状态也跟着变为rejected,已经不须要考虑pm1的执行。

3.Promise的应用

前面已经详细介绍了Promise的做用和Promise的API使用。下面将把前面介绍的东西整合,作些复杂的小样。

偶然的机会,发现了Jake Archibald很是棒的Promise实践例子,因此这里主要仍是使用他的例子,并加上一些我的的理解。

首先是基于Promise实现的ajax异步加载数据的实现,而后在这个基础上拓展成复杂的例子。

3.1在Ajax中使用Promise

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);
    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };
    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };
    // Make the request
    req.send();
  });}

咱们定义了一个get函数,传入一个url,并返回一个Promise对象。正如前面强调的同样,Promise实例对象表明一次完整的异步操做过 程。把异步代码放在function(resolve, reject) {}中。最后根据ajax的status也就异步操做的结果,决定ajax是否成功执行。

经过这样巧妙的异步代码封装到Promise实例对象中,咱们实现了ajax操做Promise化,能够实现链式使用。避免了传统的只能使用回调的麻烦。

小样:链式调用

get('story.json').then(function(response) {
  return JSON.parse(response);}).then(function(response) {
  console.log("Yey JSON!", response);});

实现了ajax的链式调用后,发现代码阅读更美观,理解起来比使用原来的回调好了不少。第一步,加载了story.json的数据;第二步,把数据转换成JSON格式;第三步,把JSON数据打印出来。

因为后面的例子都是使用JSON数据,因此须要对get进一步拓展,直接返回JSON数据:

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

3.2 Promise实现异步任务顺序执行

在实际开发中,会遇到这样的业务需求:须要把任务按照必定的顺序执行。例如:Task-1 ——> Task-2 ——> Task-3……。

例如:须要查询一个用户名为Weber用户写过的文章。

第一步:到数据库查找user name 为Weber的用户;第二步:根据Weber的用户id到数据查找Weber的文章。第三步:返回Weber的用户信息和Weber的文章
   在Java这样的IO阻塞语言中很好实现,直接按照步骤写代码便可。可是在JS中,数据库操做是异步的,每次数据库查询都要传入回调函数处理结果。

JS使用传统方式实现需求:

function getPostsByUserName(name, response) {db.findUserByName(‘weber’, function(user) {
          db.findPostByUserId(user.id, function(posts) {
              response.json(‘userPosts’, {
                   user: user,
                   posts:posts});
          });});}

回调方式能够实现功能,可是代码看起来很不美观,不容易理解,容易形成”回调黑洞”。
下面来看Promise实现异步任务顺序执行的例子:

业务需求:使用Ajax加载一篇文章信息story.json,文章信息包含各个段落的基本信息,须要在后台获取。要求在页面按照1,2,3这样的顺序显示段落。

第一步:取得文章列表story.json第二步:按照story.json文章段落列表的顺序,加载段落并按照顺序在浏览器渲染。第二步能够看作一个总体,继续细化:2-1:加载第一段落2-2:加载第二段落2-3:加载第三段落2-n:以此类推。
getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);
  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // 当前一个章节的 Promise 完成以后……
    return sequence.then(function() {
      // ……获取下一章
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // 并添加到页面
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());}).then(function() {
  // 如今所有完成了!
  addTextToPage("All done");}).catch(function(err) {
  // 若是过程当中发生任何错误
  addTextToPage("Argh, broken: " + err.message);}).then(function() {
  // 保证 spinner 最终会隐藏
  document.querySelector('.spinner').style.display = 'none';});

上述代码的效果图(请另预览git图片)
图片描述

2-1,2-2……前后的顺序,每一个Promise实例是一个异步操做,必须是前一个Promise实例完成(fulfilled或者 rejected)后一个才进行。这里巧妙的使用了promise.then方法实现了这样的控制。then方法会当即返回一个新的Promise实例对 象pm1,咱们在onFulfilled中返回下一个获取章节的Promise实例对象pm2,pm2决定了pm1的状态,也就是pm1的then方法需 要pm2的状态肯定了才会执行。以此推理,这样每一个加载章节的Promise实例就被加上前一个控制后一个的限制。

3.3 Promise实现异步任务并行执行

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);
  // 接受一个 Promise 数组并等待他们所有完成
  return Promise.all(
    // 把章节 URL 数组转换成对应的 Promise 数组
    story.chapterUrls.map(getJSON)
  );}).then(function(chapters) {
  // 如今咱们有了顺序的章节 JSON,遍历它们……
  chapters.forEach(function(chapter) {
    // ……并添加到页面中
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");}).catch(function(err) {
  // 捕获过程当中的任何错误
  addTextToPage("Argh, broken: " + err.message);}).then(function() {
  document.querySelector('.spinner').style.display = 'none';});

上述代码的效果图(请另预览git图片)
图片描述

3.2实现了异步任务的顺序执行,可是异步IO是很耗时的,假如其中一个任务很是耗时,那么后面的任务就会受到很大的影响。因此,在有些状况下,咱们须要像多线程同样,并发执行不少任务,用资源换取时间。庆幸的是,浏览器是支持ajax多任务并发执行的。

如今3.2的业务要求不变,咱们换种方法实现。注意到,业务要求是按照顺序在网页显示全部的段落。与其一个个的按照顺序加载每个段落,为何咱们不一样时把全部的段落加载过来,而后再按照顺序渲染每一个段落嗯?

每一个Promise实例对象表示一次异步操做,如今咱们把ajax Promise化了,如今咱们同时有多个异步任务,也就是多个Promise实例对象须要处理,没错,Promise.all上场了。

Promise.all中的Promise实例是并行执行的,在全部Promise实例的状态都为fulfilled的状况,Promise实例返回的值的顺序也是一致的,因此咱们能够这样实现并行加载和顺序渲染。

可是Promise.all也有个问题,假如其中一个ajax请求失败,那么总体的Promise实例就被rejected,一个任务失败致使所有段落数据得不到渲染。有时候咱们不但愿一个失败的请求对其余形成影响,当其中一个请求失败,其余请求的数据仍然按照顺序渲染。

其实很简单:让全部的Promise不管如何都是fulfilled就行了。

if (req.status == 200) {
        // 以响应文本为结果,完成此 Promise
        resolve(req.response);
      }
      else {
        //reject(Error(req.statusText));resolve(‘loading error:’ + req.statusText);
      }

方法有些猥琐,可是仍是实用的,渲染了其余数据,又给了用户错误信息提示。

某些状况下,这种方法用来避免Promise.all的这样的问题仍是不错的选择。

3.4 并行仍是顺序执行?

3.1顺序执行效率低,3.2并行执行可是须要全部的数据加载完成才能开始渲染。有没有一种鱼和熊掌兼得的方法呢?

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);
  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence.then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());}).then(function() {
  addTextToPage("All done");}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);}).then(function() {
  document.querySelector('.spinner').style.display = 'none';});

上述代码的效果图(请另预览git图片)
图片描述

从图看出,ajax并发执行,而且是按照顺序渲染的,对比3.3,第一章很快就获得更快的渲染。改善了用户体验。

reduce(function(sequence, chapterPromise) {
      return sequence.then(function() {
          return chapterPromise;
      }).then(function(chapter) {
          addHtmlToPage(chapter.html);
      });}, Promise.resolve());

(1)并行加载的实现:

story.chapterUrls.map(getJSON)把全部章节url转换成Promise实例对象。Promise实例对象被异步执行,调用function(resolve,  reject) {},也就是全部的ajax请求并发执行。

(2)顺序渲染数据的实现:

并发发送了请求,可是返回结果的顺序不是固定顺序的。因此只能从顺序渲染数据下手,必须按照章节的顺序渲染数据。为了实现这个,咱们大概想到了3-2中,顺序加载的作法,使得前一个Promise实例控制后一个Promise实例的进行。

3-3和3-4都是并发加载,可是3-4能够更快的显示第一章的数据。3-3须要把所有的数据加载完成才开始按照顺序渲染。而3-4等到加载完第一 章开始就渲染。这样的技巧,能够提升用户的体验,用户能够很快的看到第一章的内容,然后面的内容则悄悄的加载,这样的用户体验无疑是更好的。

4.总结

本文介绍了Promise的做用和Promise主要API的使用和特色,而后展现了三个使用Promise的例子,异步工做流的处理。虽然ES7 即将出现Async Await这样的异步编程利器,可是掌握Promise也是有所用处的,实际上,Promise能够和Generator和Async配合使用,使得代码 更优雅。

相关文章
相关标签/搜索