文档章节

JavaScript ES6 中的Promise

sunshinewyf
 sunshinewyf
发布于 2016/03/29 22:36
字数 4279
阅读 93
收藏 8

1. Promises/A+规范

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

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

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/async

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

2.Promise对象

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

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

2.1生成Promise实例对象

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

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配合使用,使得代码 更优雅。


本文转载自:http://www.imooc.com/article/3627

上一篇: typescript初涉
下一篇: JavaScript中的bind
sunshinewyf
粉丝 17
博文 97
码字总数 64205
作品 0
武汉
程序员
私信 提问
ES6 Promise 执行解析

作为一门单线程的语言,刚学习 JavaScript 语言的时候,我曾怀疑过 JavaScript 在处理 ajax 数据请求,文件解析等过程效率会很低,而且在执行这些任务较大的代码中,会严重阻塞后面代码的执行...

sandy_anqi
03/04
0
0
探索Javascript异步编程

笔者在之前的一片博客中简单的讨论了Python和Javascript的异同,其实作为一种编程语言Javascript的异步编程是一个非常值得讨论的有趣话题。 JavaScript 异步编程简介 回调函数和异步执行 所谓...

naughty
2014/05/22
4.3K
8
JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切

翻译:疯狂的技术宅 原文:www.valentinog.com/blog/engine… 从Call Stack,Global Memory,Event Loop,Callback Queue到 Promises 和 Async/Await 的 JavaScript引擎之旅! 你有没有想过浏...

前端先锋
05/20
0
0
异步 JavaScript - 事件循环

简评:如果你对 JavaScript 异步的原理感兴趣,这里有一篇不错的介绍。 在介绍 JavaScript 异步执行之前先来了解一下, JavaScript 同步代码是如何执行的。 这里有两个概念需要了解: 执行上...

极光推送
2018/12/05
11
0
JavaScript Promises 学习笔记

本文是 ECMAScript 2015 原生异步方法 Promise 的学习笔记。网上课程由 Udacity + Google 提供,老师是卡梅伦·皮特曼(Cameron Pittman)。 学习笔记分为 8 个部分: callbacks vs thens P...

HongyangWang
2017/04/23
0
0

没有更多内容

加载失败,请刷新页面

加载更多

OSChina 周日乱弹 —— 我,小小编辑,食人族酋长

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @宇辰OSC :分享娃娃的单曲《飘洋过海来看你》: #今日歌曲推荐# 《飘洋过海来看你》- 娃娃 手机党少年们想听歌,请使劲儿戳(这里) @宇辰OSC...

小小编辑
48分钟前
112
7
spring cloud

一、从面试题入手 1.1、什么事微服务 1.2、微服务之间如何独立通讯的 1.3、springCloud和Dubbo有哪些区别 1.通信机制:DUbbo基于RPC远程过程调用;微服务cloud基于http restFUL API 1.4、spr...

榴莲黑芝麻糊
今天
2
0
Executor线程池原理与源码解读

线程池为线程生命周期的开销和资源不足问题提供了解决方 案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。 线程实现方式 Thread、Runnable、Callable //实现Runnable接口的...

小强的进阶之路
昨天
6
0
maven 环境隔离

解决问题 即 在 resource 文件夹下面 ,新增对应的资源配置文件夹,对应 开发,测试,生产的不同的配置内容 <resources> <resource> <directory>src/main/resources.${deplo......

之渊
昨天
8
0
详解箭头函数和普通函数的区别以及箭头函数的注意事项、不适用场景

箭头函数是ES6的API,相信很多人都知道,因为其语法上相对于普通函数更简洁,深受大家的喜爱。就是这种我们日常开发中一直在使用的API,大部分同学却对它的了解程度还是不够深... 普通函数和...

OBKoro1
昨天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部