【前端知识点】promise简书-30分钟带你搞懂promise面试必备

2019年12月12日 阅读数:275
这篇文章主要向大家介绍【前端知识点】promise简书-30分钟带你搞懂promise面试必备,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

前言

写做初衷

本书的目的是以目前还在制定中的ECMAScript 6 Promises规范为中心,着重向各位读者介绍JavaScript中对Promise相关技术的支持状况。javascript

经过阅读本书,咱们但愿各位读者能在下面三个目标上有所收获。html

  • 学习Promise相关内容,能熟练使用Promise模式并进行测试html5

  • 学习Promise适合什么、不适合什么,知道Promise不是万能的,不能什么都想用Promise来解决java

  • 以ES6 Promises为基础进行学习,逐渐发展造成本身的风格node

像上面所提到的那样,本书主要是以ES6 Promises,即JavaScript的标准规范为基础的、Promise的相关知识为主要讲解内容。jquery

在Firefox和Chrome这样技术比较超前的浏览器上,不须要安装额外的插件就能使用Promise功能,此外ES6 Promises的规范来源于Promises/A+社区,它有不少版本的实现。git

咱们将会从基础API开始介绍能够在浏览器的原生支持或者经过插件支持的Promise功能。 也但愿各位读者能了解这其中Promise适合干什么,不适合干什么,能根据实际需求选择合适的技术实现方案。es6

开始阅读以前

本书的阅读对象须要对JavaScript有基本的了解和知识。github

若是你读过上面的其中一本的话,就应该很是容易理解本书的内容了。

另外若是你有使用JavaScript编写Web应用程序的经验,或者使用Node.js编写过命令行、服务器端程序的话,那么你可能会对本文中的一些内容感到很是熟悉。

本书的一本分章节将会以Node.js环境为背景进行说明,若是你有Node.js基础的话,那么必定会很是容易理解这部份内容了。

格式约定

本书为了节约篇幅,用了下面一些格式上的约定。

  • 关于Promise的术语请参考术语集

    • 通常一个名词第一次出现时都会附带相关连接。

  • 实例方法都用 instance#method 的形式。

    • 好比 Promise#then 这种写法表示的是 Promise的实例对象的 then 这一方法。

  • 对象方法都采用 object.method 的形式。

    • 这沿用了JavaScript中的使用方式,Promise.all 表示的是一个静态方法。

这部份内容主要讲述的是对正文部分的补充说明。

推荐浏览器

咱们推荐使用内置对Promise支持的浏览器来阅读本书。

Firefox和Chrome的话都支持ES6 Promises标准。

此外,虽然不是推荐的阅读环境,可是读者仍是能在iOS等移动终端上阅读本书。

运行示例代码

本网站使用了Promise的Polyfill类库,所以即便在不支持Promise的浏览器上也能执行示例代码。

此外像下面这样,各位读者能够经过运行按钮来运行可执行的示例代码。

1
var promise = new Promise(function(resolve){
2
    resolve(42);
3
});
4
promise.then(function(value){
5
    console.log(value);
6
}).catch(function(error){
7
    console.error(error);
8
});
   
42
42
42
按下   按钮以后,代码区会变成编辑区,代码也会被执行。固然你也能够经过这个按钮再次运行代码。
 按钮用于清除由  console.log 打印出来的log。 
按钮用来退出编辑模式。

若是你对哪里有疑问的话,均可以现场修改代码并执行,以加深对该部分代码的理解。

本书源代码/License

本书中示例代码均可以在GitHub上找到。

本书采用 AsciiDoc 格式编写。

此外代码仓库中还包含本书示例代码的测试代码。

源代码的许可证为MIT许可证,文章内容能够基于CC-BY-NC使用。

意见和疑问

若是有意见或者问题的话,能够直接在GitHub上提Issue便可。

此外,你也能够在 在线聊天 上留言。

  • Gitter

各位读者除了能免费阅读本书,也有编辑本书的权利。你能够在GitHub上经过 Pull Requests 来贡献本身的工做。

1. Chapter.1 - 什么是Promise

本章将主要对JavaScript中的Promise进行入门级的介绍。

1.1. 什么是Promise

首先让咱们来了解一下到底什么是Promise。

Promise是抽象异步处理对象以及对其进行各类操做的组件。 其详细内容在接下来咱们还会进行介绍,Promise并非从JavaScript中发祥的概念。

Promise最初被提出是在 E语言中, 它是基于并列/并行处理设计的一种编程语言。

如今JavaScript也拥有了这种特性,这就是本书所介绍的JavaScript Promise。

另外,若是说到基于JavaScript的异步处理,我想大多数都会想到利用回调函数。

使
----
getAsync("fileA.txt", function(error, result){
    if(error){// 取得失败时的处理
        throw error;
    }
    // 取得成功时的处理
});
----
<1> (error )

Node.js等则规定在JavaScript的回调函数的第一个参数为 Error 对象,这也是它的一个惯例。

像上面这样基于回调函数的异步处理若是统一参数使用规则的话,写法也会很明了。 可是,这也仅是编码规约而已,即便采用不一样的写法也不会出错。

而Promise则是把相似的异步处理对象和处理规则进行规范化, 并按照采用统一的接口来编写,而采起规定方法以外的写法都会出错。

使Promise
----
var promise = getAsyncPromise("fileA.txt"); 
promise.then(function(result){
    // 获取文件内容成功时的处理
}).catch(function(error){
    // 获取文件内容失败时的处理
});
----
<1> promise

咱们能够向这个预设了抽象化异步处理的promise对象, 注册这个promise对象执行成功时和失败时相应的回调函数。

这和回调函数方式相比有哪些不一样之处呢? 在使用promise进行一步处理的时候,咱们必须按照接口规定的方法编写处理代码。

也就是说,除promise对象规定的方法(这里的 then 或 catch)之外的方法都是不能够使用的, 而不会像回调函数方式那样能够本身自由的定义回调函数的参数,而必须严格遵照固定、统一的编程方式来编写代码。

这样,基于Promise的统一接口的作法, 就能够造成基于接口的各类各样的异步处理模式。

因此,promise的功能是能够将复杂的异步处理轻松地进行模式化, 这也能够说得上是使用promise的理由之一。

接下来,让咱们在实践中来学习JavaScript的Promise吧。

1.2. Promise简介

在 ES6 Promises 标准中定义的API还不是不少。

目前大体有下面三种类型。

Constructor

Promise相似于 XMLHttpRequest,从构造函数 Promise 来建立一个新建新promise对象做为接口。

要想建立一个promise对象、能够使用new来调用Promise的构造器来进行实例化。

var promise = new Promise(function(resolve, reject) {
    // 异步处理
    // 处理结束后、调用resolve 或 reject
});

Instance Method

对经过new生成的promise对象为了设置其值在 resolve(成功) / reject(失败)时调用的回调函数 能够使用promise.then() 实例方法。

promise.then(onFulfilled, onRejected)
resolve(成功)时

onFulfilled 会被调用

reject(失败)时

onRejected 会被调用

onFulfilledonRejected 两个都为可选参数。

promise.then 成功和失败时均可以使用。 另外在只想对异常进行处理时能够采用 promise.then(undefined, onRejected) 这种方式,只指定reject时的回调函数便可。 不过这种状况下 promise.catch(onRejected) 应该是个更好的选择。

promise.catch(onRejected)

Static Method

像 Promise 这样的全局对象还拥有一些静态方法。

包括 Promise.all() 还有 Promise.resolve() 等在内,主要都是一些对Promise进行操做的辅助方法。

1.2.1. Promise workflow

咱们先来看一看下面的示例代码。

promise-workflow.js
function asyncFunction() {
    
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            resolve('Async Hello world');
        }, 16);
    });
}

asyncFunction().then(function (value) {
    console.log(value);    // => 'Async Hello world'
}).catch(function (error) {
    console.log(error);
});
new Promise构造器以后,会返回一个promise对象
<1>为promise对象用设置 .then 调用返回值时的回调函数。

asyncFunction 这个函数会返回promise对象, 对于这个promise对象,咱们调用它的 then 方法来设置resolve后的回调函数, catch 方法来设置发生错误时的回调函数。

该promise对象会在setTimeout以后的16ms时被resolve, 这时 then 的回调函数会被调用,并输出 'Async Hello world' 。

在这种状况下 catch 的回调函数并不会被执行(由于promise返回了resolve), 不过若是运行环境没有提供 setTimeout 函数的话,那么上面代码在执行中就会产生异常,在 catch 中设置的回调函数就会被执行。

固然,像promise.then(onFulfilled, onRejected) 的方法声明同样, 若是不使用catch 方法只使用 then方法的话,以下所示的代码也能完成相同的工做。

asyncFunction().then(function (value) {
    console.log(value);
}, function (error) {
    console.log(error);
});

1.2.2. Promise的状态

咱们已经大概了解了Promise的处理流程,接下来让咱们来稍微整理一下Promise的状态。

new Promise 实例化的promise对象有如下三个状态。

"has-resolution" - Fulfilled

resolve(成功)时。此时会调用 onFulfilled

"has-rejection" - Rejected

reject(失败)时。此时会调用 onRejected

"unresolved" - Pending

既不是resolve也不是reject的状态。也就是promise对象刚被建立后的初始化状态等

关于上面这三种状态的读法,其中 左侧为在 ES6 Promises 规范中定义的术语, 而右侧则是在 Promises/A+ 中描述状态的术语。

基本上状态在代码中是不会涉及到的,因此名称也无需太在乎。 在这本书中,咱们会基于 Promises/A+ 中 Pending 、 Fulfilled 、 Rejected 的状态名称进行讲述。

promise-states
Figure 1. promise states

在 ECMAScript Language Specification ECMA-262 6th Edition – DRAFT 中 [[PromiseStatus]] 都是在内部定义的状态。 因为没有公开的访问 [[PromiseStatus]] 的用户API,因此暂时尚未查询其内部状态的方法。

到此在本文中咱们已经介绍了promise全部的三种状态。

promise对象的状态,从Pending转换为FulfilledRejected以后, 这个promise对象的状态就不会再发生任何变化。

也就是说,Promise与Event等不一样,在.then 后执行的函数能够确定地说只会被调用一次。

另外,FulfilledRejected这两个中的任一状态均可以表示为Settled(不变的)。

Settled

resolve(成功) 或 reject(失败)。

PendingSettled的对称关系来看,Promise状态的种类/迁移是很是简单易懂的。

当promise的对象状态发生变化时,用.then 来定义只会被调用一次的函数。

JavaScript Promises - Thinking Sync in an Async World // Speaker Deck 这个ppt中有关于Promise状态迁移的很是容易理解的说明。

1.3. 编写Promise代码

这里咱们来介绍一下如何编写Promise代码。

1.3.1. 建立promise对象

建立promise对象的流程以下所示。

  1. new Promise(fn) 返回一个promise对象

  2. fn 中指定异步等处理

    • 处理结果正常的话,调用resolve(处理结果值)

    • 处理结果错误的话,调用reject(Error对象)

按这个流程咱们来实际编写下promise代码吧。

咱们的任务是用Promise来经过异步处理方式来获取XMLHttpRequest(XHR)的数据。

建立XHR的promise对象

首先,建立一个用Promise把XHR处理包装起来的名为 getURL 的函数。

xhr-promise.js
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
// 运行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
    console.log(value);
}).catch(function onRejected(error){
    console.error(error);
});

getURL 只有在经过XHR取得结果状态为200时才会调用 resolve - 也就是只有数据取得成功时,而其余状况(取得失败)时则会调用 reject 方法。

resolve(req.responseText) 在response的内容中加入了参数。 resolve方法的参数并无特别的规则,基本上把要传给回调函数参数放进去就能够了。 ( then 方法能够接收到这个参数值)

熟悉Node.js的人,常常会在写回调函数时将 callback(error, response) 的第一个参数设为error对象,而在Promise中resolve/reject则担当了这个职责(处理正常和异常的状况),因此 在resolve方法中只传一个response参数是没有问题的。

接下来咱们来看一下reject函数。

XHR中 onerror 事件被触发的时候就是发生错误时,因此理所固然调用reject。 这里咱们重点来看一下传给reject的值。

发生错误时要像这样 reject(new Error(req.statusText)); ,建立一个Error对象后再将具体的值传进去。 传给reject 的参数也没有什么特殊的限制,通常只要是Error对象(或者继承自Error对象)就能够。

传给reject 的参数,其中通常是包含了reject缘由的Error对象。 本次由于状态值不等于200而被reject,因此reject 中放入的是statusText。 (这个参数的值能够被 then 方法的第二个参数或者 catch 方法中使用)

1.3.2. 编写promise对象处理方法

让咱们在实际中使用一下刚才建立的返回promise对象的函数

getURL("http://example.com/"); // => 返回promise对象

Promises Overview 中作的简单介绍同样,promise对象拥有几个实例方法, 咱们使用这些实例方法来为promise对象建立依赖于promise的具体状态、而且只会被执行一次的回调函数。

为promise对象添加处理方法主要有如下两种

  • promise对象被 resolve 时的处理(onFulfilled)

  • promise对象被 reject 时的处理(onRejected)

promise-resolve-flow
Figure 2. promise value flow

首先,咱们来尝试一下为 getURL 通讯成功并取到值时添加的处理函数。

此时所谓的 通讯成功 , 指的就是在被resolve后, promise对象变为FulFilled状态 。

resolve后的处理,能够在.then 方法中传入想要调用的函数。

var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){ 
    console.log(value);
});
为了方便理解咱们把函数命名为 onFulfilled

getURL函数 中的 resolve(req.responseText); 会将promise对象变为resolve(Fulfilled)状态, 同时使用其值调用 onFulfilled函数。

不过目前咱们尚未对其中可能发生的错误作任何处理, 接下来,咱们就来为 getURL 函数添加发生错误时的异常处理。

此时 发生错误 , 指的也就是reject后 promise对象变为Rejected状态 。

reject后的处理,能够在.then 的第二个参数 或者是在 .catch 方法中设置想要调用的函数。

把下面reject时的处理加入到刚才的代码,以下所示。

var URL = "http://httpbin.org/status/500"; 
getURL(URL).then(function onFulfilled(value){
    console.log(value);
}).catch(function onRejected(error){ 
    console.error(error);
});
服务端返回的状态码为500
为了方便理解函数被命名为 onRejected

getURL 的处理中发生任何异常,或者被明确reject的状况下, 该异常缘由(Error对象)会做为 .catch 方法的参数被调用。

其实 .catch只是 promise.then(undefined, onRejected) 的别名而已, 以下代码也能够完成一样的功能。

getURL(URL).then(onFulfilled, onRejected);
onFulfilled, onRejected 是和刚才相同的函数

通常说来,使用.catch来将resolve和reject处理分开来写是比较推荐的作法, 这二者的区别会在then和catch的区别中再作详细介绍。

总结

在本章咱们简单介绍了如下内容:

  • 用 new Promise 方法建立promise对象

  • .then 或 .catch 添加promise对象的处理函数

到此为止咱们已经学习了Promise的基本写法。 其余不少处理都是由此基本语法延伸的,也使用了Promise提供的一些静态方法来实现。

实际上即便使用回调方式的写法也能完成上面一样的工做,而使用Promise方式的话有什么优势么?在本小节中咱们没有讲到二者的对比及Promise的优势。在接下来的章节中,咱们将会对Promise优势之一,即错误处理机制进行介绍,以及和传统的回调方式的对比。

2. Chapter.2 - 实战Promise

本章咱们将会学习Promise提供的各类方法以及如何进行错误处理。

2.1. Promise.resolve

通常状况下咱们都会使用 new Promise() 来建立promise对象,可是除此以外咱们也能够使用其余方法。

在这里,咱们将会学习如何使用 Promise.resolve 和 Promise.reject这两个方法。

2.1.1. new Promise的快捷方式

静态方法Promise.resolve(value) 能够认为是 new Promise() 方法的快捷方式。

好比 Promise.resolve(42); 能够认为是如下代码的语法糖。

new Promise(function(resolve){
    resolve(42);
});

在这段代码中的 resolve(42); 会让这个promise对象当即进入肯定(即resolved)状态,并将 42 传递给后面then里所指定的 onFulfilled 函数。

方法 Promise.resolve(value); 的返回值也是一个promise对象,因此咱们能够像下面那样接着对其返回值进行 .then 调用。

Promise.resolve(42).then(function(value){
    console.log(value);
});

Promise.resolve做为 new Promise() 的快捷方式,在进行promise对象的初始化或者编写测试代码的时候都很是方便。

2.1.2. Thenable

Promise.resolve 方法另外一个做用就是将 thenable 对象转换为promise对象。

ES6 Promises里提到了Thenable这个概念,简单来讲它就是一个很是相似promise的东西。

就像咱们有时称具备 .length 方法的非数组对象为Array like同样,thenable指的是一个具备 .then 方法的对象。

这种将thenable对象转换为promise对象的机制要求thenable对象所拥有的 then 方法应该和Promise所拥有的 then 方法具备一样的功能和处理过程,在将thenable对象转换为promise对象的时候,还会巧妙的利用thenable对象原来具备的 then 方法。

到底什么样的对象能算是thenable的呢,最简单的例子就是 jQuery.ajax(),它的返回值就是thenable的。

由于jQuery.ajax() 的返回值是 jqXHR Object 对象,这个对象具备 .then 方法。

$.ajax('/json/comment.json');// => 拥有 `.then` 方法的对象

这个thenable的对象能够使用 Promise.resolve 来转换为一个promise对象。

变成了promise对象的话,就能直接使用 then 或者 catch 等这些在 ES6 Promises里定义的方法了。

将thenable对象转换promise对象
var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise对象
promise.then(function(value){
   console.log(value);
});
jQuery和thenable

jQuery.ajax()的返回值是一个具备 .then 方法的 jqXHR Object对象,这个对象继承了来自 Deferred Object 的方法和属性。

可是Deferred Object并无遵循Promises/A+ES6 Promises标准,因此即便看上去这个对象转换成了一个promise对象,可是会出现缺失部分信息的问题。

这个问题的根源在于jQuery的 Deferred Object 的 then 方法机制与promise不一样。

因此咱们应该注意,即便一个对象具备 .then 方法,也不必定就能做为ES6 Promises对象使用。

Promise.resolve 只使用了共通的方法 then ,提供了在不一样的类库之间进行promise对象互相转换的功能。

这种转换为thenable的功能在以前是经过使用 Promise.cast 来完成的,从它的名字咱们也不难想象它的功能是什么。

除了在编写使用Promise的类库等软件时须要对Thenable有所了解以外,一般做为end-user使用的时候,咱们可能不会用到此功能。

咱们会在后面第4章的Promise.resolve和Thenable中进行详细的说明,介绍一下结合使用了Thenable和Promise.resolve的具体例子。

简单总结一下 Promise.resolve 方法的话,能够认为它的做用就是将传递给它的参数填充(Fulfilled)到promise对象后并返回这个promise对象。

此外,Promise的不少处理内部也是使用了 Promise.resolve 算法将值转换为promise对象后再进行处理的。

2.2. Promise.reject

Promise.reject(error)是和 Promise.resolve(value) 相似的静态方法,是 new Promise() 方法的快捷方式。

好比 Promise.reject(new Error("出错了")) 就是下面代码的语法糖形式。

new Promise(function(resolve,reject){
    reject(new Error("出错了"));
});

这段代码的功能是调用该promise对象经过then指定的 onRejected 函数,并将错误(Error)对象传递给这个 onRejected 函数。

Promise.reject(new Error("BOOM!")).catch(function(error){
    console.error(error);
});

它和Promise.resolve(value) 的不一样之处在于promise内调用的函数是reject而不是resolve,这在编写测试代码或者进行debug时,说不定会用得上。

2.3. 专栏: Promise只能进行异步操做?

在使用Promise.resolve(value) 等方法的时候,若是promise对象马上就能进入resolve状态的话,那么你是否是以为 .then 里面指定的方法就是同步调用的呢?

实际上, .then 中指定的方法调用是异步进行的。

var promise = new Promise(function (resolve){
    console.log("inner promise"); // 1
    resolve(42);
});
promise.then(function(value){
    console.log(value); // 3
});
console.log("outer promise"); // 2

执行上面的代码会输出下面的log,从这些log咱们清楚地知道了上面代码的执行顺序。

inner promise // 1
outer promise // 2
42            // 3

因为JavaScript代码会按照文件的从上到下的顺序执行,因此最开始 <1> 会执行,而后是 resolve(42); 被执行。这时候 promise 对象的已经变为肯定状态,FulFilled被设置为了 42 。

下面的代码 promise.then 注册了 <3> 这个回调函数,这是本专栏的焦点问题。

因为 promise.then 执行的时候promise对象已是肯定状态,从程序上说对回调函数进行同步调用也是行得通的。

可是即便在调用 promise.then 注册回调函数的时候promise对象已是肯定的状态,Promise也会以异步的方式调用该回调函数,这是在Promise设计上的规定方针。

所以 <2> 会最早被调用,最后才会调用回调函数 <3> 。

为何要对明明能够以同步方式进行调用的函数,非要使用异步的调用方式呢?

2.3.1. 同步调用和异步调用同时存在致使的混乱

其实在Promise以外也存在这个问题,这里咱们以通常的使用状况来考虑此问题。

这个问题的本质是接收回调函数的函数,会根据具体的执行状况,能够选择是以同步仍是异步的方式对回调函数进行调用。

下面咱们以 onReady(fn) 为例进行说明,这个函数会接收一个回调函数进行处理。

mixed-onready.js
function onReady(fn) {
    var readyState = document.readyState;
    if (readyState === 'interactive' || readyState === 'complete') {
        fn();
    } else {
        window.addEventListener('DOMContentLoaded', fn);
    }
}
onReady(function () {
    console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');

mixed-onready.js会根据执行时DOM是否已经装载完毕来决定是对回调函数进行同步调用仍是异步调用。

若是在调用onReady以前DOM已经载入的话

对回调函数进行同步调用

若是在调用onReady以前DOM尚未载入的话

经过注册 DOMContentLoaded 事件监听器来对回调函数进行异步调用

所以,若是这段代码在源文件中出现的位置不一样,在控制台上打印的log消息顺序也会不一样。

为了解决这个问题,咱们能够选择统一使用异步调用的方式。

async-onready.js
function onReady(fn) {
    var readyState = document.readyState;
    if (readyState === 'interactive' || readyState === 'complete') {
        setTimeout(fn, 0);
    } else {
        window.addEventListener('DOMContentLoaded', fn);
    }
}
onReady(function () {
    console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');

关于这个问题,在 Effective JavaScript 的 第67项 不要对异步回调函数进行同步调用 中也有详细介绍。

  • 绝对不能对异步回调函数(即便在数据已经就绪)进行同步调用。

  • 若是对异步回调函数进行同步调用的话,处理顺序可能会与预期不符,可能带来意料以外的后果。

  • 对异步回调函数进行同步调用,还可能致使栈溢出或异常处理错乱等问题。

  • 若是想在未来某时刻调用异步回调函数的话,能够使用 setTimeout 等异步API。

Effective JavaScript— David Herman

前面咱们看到的 promise.then 也属于此类,为了不上述中同时使用同步、异步调用可能引发的混乱问题,Promise在规范上规定 Promise只能使用异步调用方式 。

最后,若是将上面的 onReady 函数用Promise重写的话,代码以下面所示。

onready-as-promise.js
function onReadyPromise() {
    return new Promise(function (resolve, reject) {
        var readyState = document.readyState;
        if (readyState === 'interactive' || readyState === 'complete') {
            resolve();
        } else {
            window.addEventListener('DOMContentLoaded', resolve);
        }
    });
}
onReadyPromise().then(function () {
    console.log('DOM fully loaded and parsed');
});
console.log('==Starting==');

因为Promise保证了每次调用都是以异步方式进行的,因此咱们在实际编码中不须要调用 setTimeout 来本身实现异步调用。

2.4. Promise#then

在前面的章节里咱们对Promise基本的实例方法 then 和 catch 的使用方法进行了说明。

这其中,我想你们已经认识了 .then().catch() 这种链式方法的写法了,其实在Promise里能够将任意个方法连在一块儿做为一个方法链(method chain)。

promise能够写成方法链的形式
aPromise.then(function taskA(value){
// task A
}).then(function taskB(vaue){
// task B
}).catch(function onRejected(error){
    console.log(error);
});

若是把在 then 中注册的每一个回调函数称为task的话,那么咱们就能够经过Promise方法链方式来编写能以taskA → task B 这种流程进行处理的逻辑了。

Promise方法链这种叫法有点长(实际上是在日语里有点长,中文还能够 --译者注),所以后面咱们会简化为 promise chain 这种叫法。

Promise之因此适合编写异步处理较多的应用,promise chain能够算得上是其中的一个缘由吧。

在本小节,咱们将主要针对使用 then 的promise chain的行为和流程进行学习。

2.4.1. promise chain

在第一章 promise chain 里咱们看到了一个很简单的 then → catch 的例子,若是咱们将方法链的长度变得更长的话,那在每一个promise对象中注册的onFulfilled和onRejected将会怎样执行呢?

promise chain - 即方法链越短越好。 在这个例子里咱们是为了方便说明才选择了较长的方法链。

咱们先来看看下面这样的promise chain。

promise-then-catch-flow.js
function taskA() {
    console.log("Task A");
}
function taskB() {
    console.log("Task B");
}
function onRejected(error) {
    console.log("Catch Error: A or B", error);
}
function finalTask() {
    console.log("Final Task");
}

var promise = Promise.resolve();
promise
    .then(taskA)
    .then(taskB)
    .catch(onRejected)
    .then(finalTask);

上面代码中的promise chain的执行流程,若是用一张图来描述一下的话,像下面的图那样。

promise-then-catch-flow
Figure 3. promise-then-catch-flow.js附图

在 上述代码 中,咱们没有为 then 方法指定第二个参数(onRejected),也能够像下面这样来理解。

then

注册onFulfilled时的回调函数

catch

注册onRejected时的回调函数

再看一下 上面的流程图 的话,咱们会发现 Task A 和 Task B 都有指向 onRejected 的线出来。

这些线的意思是在 Task A 或 Task B 的处理中,在下面的状况下就会调用 onRejected 方法。

  • 发生异常的时候

  • 返回了一个Rejected状态的promise对象

在 第一章 中咱们已经看到,Promise中的处理习惯上都会采用 try-catch 的风格,当发生异常的时候,会被 catch 捕获并被由在此函数注册的回调函数进行错误处理。

另外一种异常处理策略是经过 返回一个Rejected状态的promise对象 来实现的,这种方法不经过使用 throw 就能在promise chain中对 onRejected 进行调用。

关于这种方法因为和本小节关系不大就不在这里详述了,你们能够参考一下第4章 使用reject而不是throw 中的内容。

此外在promise chain中,因为在 onRejected 和 Final Task 后面没有 catch 处理了,所以在这两个Task中若是出现异常的话将不会被捕获,这点须要注意一下。

下面咱们再来看一个具体的关于 Task A → onRejected 的例子。

Task A产生异常的例子

Task A 处理中发生异常的话,会按照TaskA → onRejected → FinalTask 这个流程来进行处理。

promise taska rejected flow
Figure 4. Task A产生异常时的示意图

将上面流程写成代码的话以下所示。

promise-then-taska-throw.js
function taskA() {
    console.log("Task A");
    throw new Error("throw Error @ Task A")
}
function taskB() {
    console.log("Task B");// 不会被调用
}
function onRejected(error) {
    console.log(error);// => "throw Error @ Task A"
}
function finalTask() {
    console.log("Final Task");
}

var promise = Promise.resolve();
promise
    .then(taskA)
    .then(taskB)
    .catch(onRejected)
    .then(finalTask);

执行这段代码咱们会发现 Task B 是不会被调用的。

在本例中咱们在taskA中使用了 throw 方法故意制造了一个异常。但在实际中想主动进行onRejected调用的时候,应该返回一个Rejected状态的promise对象。关于这种两种方法的异同,请参考 使用reject而不是throw 中的讲解。

2.4.2. promise chain 中如何传递参数

前面例子中的Task都是相互独立的,只是被简单调用而已。

这时候若是 Task A 想给 Task B 传递一个参数该怎么办呢?

答案很是简单,那就是在 Task A 中 return 的返回值,会在 Task B 执行时传给它。

咱们仍是先来看一个具体例子吧。

promise-then-passing-value.js
function doubleUp(value) {
    return value * 2;
}
function increment(value) {
    return value + 1;
}
function output(value) {
    console.log(value);// => (1 + 1) * 2
}

var promise = Promise.resolve(1);
promise
    .then(increment)
    .then(doubleUp)
    .then(output)
    .catch(function(error){
        // promise chain中出现异常的时候会被调用
        console.error(error);
    });

这段代码的入口函数是 Promise.resolve(1); ,总体的promise chain执行流程以下所示。

  1. Promise.resolve(1); 传递 1 给 increment 函数

  2. 函数 increment 对接收的参数进行 +1 操做并返回(经过return

  3. 这时参数变为2,并再次传给 doubleUp 函数

  4. 最后在函数 output 中打印结果

promise-then-passing-value
Figure 5. promise-then-passing-value.js示意图

每一个方法中 return 的值不只只局限于字符串或者数值类型,也能够是对象或者promise对象等复杂类型。

return的值会由 Promise.resolve(return的返回值); 进行相应的包装处理,所以无论回调函数中会返回一个什么样的值,最终 then的结果都是返回一个新建立的promise对象。

关于这部份内容能够参考 专栏: 每次调用then都会返回一个新建立的promise对象 ,那里也对一些常见错误进行了介绍。

也就是说, Promise#then 不只仅是注册一个回调函数那么简单,它还会将回调函数的返回值进行变换,建立并返回一个promise对象。

2.5. Promise#catch

在 前面的Promise#then 的章节里,咱们已经简单地使用了 Promise#catch 方法。

这里咱们再说一遍,实际上 Promise#catch 只是 promise.then(undefined, onRejected); 方法的一个别名而已。 也就是说,这个方法用来注册当promise对象状态变为Rejected时的回调函数。

关于如何根据场景使用 Promise#then 和 Promise#catch 能够参考 then or catch? 中介绍的内容。

2.5.1. IE8的问题

Build Status

上面的这张图,是下面这段代码在使用 polyfill 的状况下在个浏览器上执行的结果。

polyfill是一个支持在不具有某一功能的浏览器上使用该功能的Library。 这里咱们使用的例子则来源于 jakearchibald/es6-promise 。

Promise#catch的运行结果
var promise = Promise.reject(new Error("message"));
promise.catch(function (error) {
    console.error(error);
});

若是咱们在各类浏览器中执行这段代码,那么在IE8及如下版本则会出现 identifier not found 的语法错误。

这是怎么回事呢? 实际上这和 catch 是ECMAScript的 保留字 (Reserved Word)有关。

在ECMAScript 3中保留字是不能做为对象的属性名使用的。 而IE8及如下版本都是基于ECMAScript 3实现的,所以不能将 catch 做为属性来使用,也就不能编写相似 promise.catch() 的代码,所以就出现了 identifier not found 这种语法错误了。

而如今的浏览器都是基于ECMAScript 5的,而在ECMAScript 5中保留字都属于 IdentifierName ,也能够做为属性名使用了。

在ECMAScript5中保留字也不能做为 Identifier 即变量名或方法名使用。 若是咱们定义了一个名为 for 的变量的话,那么就不能和循环语句的 for 区分了。 而做为属性名的话,咱们仍是很容易区分 object.for 和 for 的,仔细想一想咱们就应该能接受将保留字做为属性名来使用了。

固然,咱们也能够想办法回避这个ECMAScript 3保留字带来的问题。

点标记法(dot notation) 要求对象的属性必须是有效的标识符(在ECMAScript 3中则不能使用保留字),

可是使用 中括号标记法(bracket notation)的话,则能够将非合法标识符做为对象的属性名使用。

也就是说,上面的代码若是像下面这样重写的话,就能在IE8及如下版本的浏览器中运行了(固然还须要polyfill)。

解决Promise#catch标识符冲突问题
var promise = Promise.reject(new Error("message"));
promise["catch"](function (error) {
    console.error(error);
});

或者咱们不单纯的使用 catch ,而是使用 then 也是能够避免这个问题的。

使用Promise#then代替Promise#catch
var promise = Promise.reject(new Error("message"));
promise.then(undefined, function (error) {
    console.error(error);
});

因为 catch 标识符可能会致使问题出现,所以一些类库(Library)也采用了 caught 做为函数名,而函数要完成的工做是同样的。

并且不少压缩工具自带了将 promise.catch 转换为 promise["catch"] 的功能, 因此可能不经意之间也能帮咱们解决这个问题。

若是各位读者须要支持IE8及如下版本的浏览器的话,那么必定要将这个 catch 问题牢记在心中。

2.6. 专栏: 每次调用then都会返回一个新建立的promise对象

从代码上乍一看, aPromise.then(...).catch(...) 像是针对最初的 aPromise 对象进行了一连串的方法链调用。

然而实际上无论是 then 仍是 catch 方法调用,都返回了一个新的promise对象。

下面咱们就来看看如何确认这两个方法返回的究竟是不是新的promise对象。

var aPromise = new Promise(function (resolve) {
    resolve(100);
});
var thenPromise = aPromise.then(function (value) {
    console.log(value);
});
var catchPromise = thenPromise.catch(function (error) {
    console.error(error);
});
console.log(aPromise !== thenPromise); // => true
console.log(thenPromise !== catchPromise);// => true

=== 是严格相等比较运算符,咱们能够看出这三个对象都是互不相同的,这也就证实了 then 和 catch 都返回了和调用者不一样的promise对象。

Then Catch flow

咱们在对Promise进行扩展的时候须要紧紧记住这一点,不然稍不留神就有可能对错误的promise对象进行了处理。

若是咱们知道了 then 方法每次都会建立并返回一个新的promise对象的话,那么咱们就应该不难理解下面代码中对 then 的使用方式上的差异了。

// 1: 对同一个promise对象同时调用 `then` 方法
var aPromise = new Promise(function (resolve) {
    resolve(100);
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    return value * 2;
});
aPromise.then(function (value) {
    console.log("1: " + value); // => 100
})

// vs

// 2: 对 `then` 进行 promise chain 方式进行调用
var bPromise = new Promise(function (resolve) {
    resolve(100);
});
bPromise.then(function (value) {
    return value * 2;
}).then(function (value) {
    return value * 2;
}).then(function (value) {
    console.log("2: " + value); // => 100 * 2 * 2
});

第1种写法中并无使用promise的方法链方式,这在Promise中是应该极力避免的写法。这种写法中的 then 调用几乎是在同时开始执行的,并且传给每一个 then 方法的 value 值都是 100 。

第2中写法则采用了方法链的方式将多个 then 方法调用串连在了一块儿,各函数也会严格按照 resolve → then → then → then 的顺序执行,而且传给每一个 then 方法的 value 的值都是前一个promise对象经过 return 返回的值。

下面是一个由方法1中的 then 用法致使的比较容易出现的颇有表明性的反模式的例子。

✘  then 的错误使用方法
function badAsyncCall() {
    var promise = Promise.resolve();
    promise.then(function() {
        // 任意处理
        return newVar;
    });
    return promise;
}

这种写法有不少问题,首先在 promise.then 中产生的异常不会被外部捕获,此外,也不能获得 then 的返回值,即便其有返回值。

因为每次 promise.then 调用都会返回一个新建立的promise对象,所以须要像上述方式2那样,采用promise chain的方式将调用进行链式化,修改后的代码以下所示。

then 返回返回新建立的promise对象
function anAsyncCall() {
    var promise = Promise.resolve();
    return promise.then(function() {
        // 任意处理
        return newVar;
    });
}

关于这些反模式,详细内容能够参考 Promise Anti-patterns 。

这种函数的行为贯穿在Promise总体之中, 包括咱们后面要进行说明的 Promise.all 和 Promise.race ,他们都会接收一个promise对象为参数,并返回一个和接收参数不一样的、新的promise对象。

2.7. Promise和数组

到目前为止咱们已经学习了如何经过 .then 和 .catch 来注册回调函数,这些回调函数会在promise对象变为 FulFilled 或 Rejected 状态以后被调用。

若是只有一个promise对象的话咱们能够像前面介绍的那样编写代码就能够了,若是要在多个promise对象都变为FulFilled状态的时候才要进行某种处理话该如何操做呢?

咱们以当全部XHR(异步处理)所有结束后要进行某操做为例来进行说明。

各位读者如今也许有点难以在大脑中描绘出这么一种场景,咱们能够先看一下下面使用了普通的回调函数风格的XHR处理代码。

2.7.1. 经过回调方式来进行多个异步调用

multiple-xhr-callback.js
function getURLCallback(URL, callback) {
    var req = new XMLHttpRequest();
    req.open('GET', URL, true);
    req.onload = function () {
        if (req.status === 200) {
            callback(null, req.responseText);
        } else {
            callback(new Error(req.statusText), req.response);
        }
    };
    req.onerror = function () {
        callback(new Error(req.statusText));
    };
    req.send();
}
// <1> 对JSON数据进行安全的解析
function jsonParse(callback, error, value) {
    if (error) {
        callback(error, value);
    } else {
        try {
            var result = JSON.parse(value);
            callback(null, result);
        } catch (e) {
            callback(e, value);
        }
    }
}
// <2> 发送XHR请求
var request = {
        comment: function getComment(callback) {
            return getURLCallback('http://azu.github.io/promises-book/json/comment.json', jsonParse.bind(null, callback));
        },
        people: function getPeople(callback) {
            return getURLCallback('http://azu.github.io/promises-book/json/people.json', jsonParse.bind(null, callback));
        }
    };
// <3> 启动多个XHR请求,当全部请求返回时调用callback
function allRequest(requests, callback, results) {
    if (requests.length === 0) {
        return callback(null, results);
    }
    var req = requests.shift();
    req(function (error, value) {
        if (error) {
            callback(error, value);
        } else {
            results.push(value);
            allRequest(requests, callback, results);
        }
    });
}
function main(callback) {
    allRequest([request.comment, request.people], callback, []);
}
// 运行的例子
main(function(error, results){
    if(error){
        return console.error(error);
    }
    console.log(results);
});

这段回调函数风格的代码有如下几个要点。

  • 直接使用 JSON.parse 函数的话可能会抛出异常,因此这里使用了一个包装函数 jsonParse

  • 若是将多个XHR处理进行嵌套调用的话层次会比较深,因此使用了 allRequest 函数并在其中对request进行调用。

  • 回调函数采用了 callback(error,value) 这种写法,第一个参数表示错误信息,第二个参数为返回值

在使用 jsonParse 函数的时候咱们使用了 bind 进行绑定,经过使用这种偏函数(Partial Function)的方式就能够减小匿名函数的使用。(若是在函数回调风格的代码能很好的作到函数分离的话,也能减小匿名函数的数量)

jsonParse.bind(null, callback);
// 能够认为这种写法能转换为如下的写法
function bindJSONParse(error, value){
    jsonParse(callback, error, value);
}

在这段回调风格的代码中,咱们也能发现以下一些问题。

  • 须要显示进行异常处理

  • 为了避免让嵌套层次太深,须要一个对request进行处理的函数

  • 处处都是回调函数

下面咱们再来看看如何使用 Promise#then 来完成一样的工做。

2.7.2. 使用Promise#then同时处理多个异步请求

须要事先说明的是 Promise.all 比较适合这种应用场景的需求,所以咱们故意采用了大量 .then 的晦涩的写法。

使用了.then 的话,也并非说能和回调风格彻底一致,大概重写后代码以下所示。

multiple-xhr.js
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };
function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    // [] 用来保存初始化的值
    var pushValue = recordValue.bind(null, []);
    return request.comment().then(pushValue).then(request.people).then(pushValue);
}
// 运行的例子
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

将上述代码和回调函数风格相比,咱们能够获得以下结论。

  • 能够直接使用 JSON.parse 函数

  • 函数 main() 返回promise对象

  • 错误处理的地方直接对返回的promise对象进行处理

向前面咱们说的那样,main的 then 部分有点晦涩难懂。

为了应对这种须要对多个异步调用进行统一处理的场景,Promise准备了 Promise.all 和 Promise.race 这两个静态方法。

在下面的小节中咱们将对这两个函数进行说明。

2.8. Promise.all

Promise.all 接收一个 promise对象的数组做为参数,当这个数组里的全部promise对象所有变为resolve或reject状态的时候,它才会去调用 .then 方法。

前面咱们看到的批量得到若干XHR的请求结果的例子,使用 Promise.all 的话代码会很是简单。

以前例子中的 getURL 返回了一个promise对象,它封装了XHR通讯的实现。 向 Promise.all 传递一个由封装了XHR通讯的promise对象数组的话,则只有在所有的XHR通讯完成以后(变为FulFilled或Rejected状态)以后,才会调用 .then 方法。

promise-all-xhr.js
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };
function main() {
    return Promise.all([request.comment(), request.people()]);
}
// 运行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.log(error);
});

这个例子的执行方法和 前面的例子 同样。 不过Promise.all 在如下几点和以前的例子有所不一样。

  • main中的处理流程显得很是清晰

  • Promise.all 接收 promise对象组成的数组做为参数

Promise.all([request.comment(), request.people()]);

在上面的代码中,request.comment() 和 request.people() 会同时开始执行,并且每一个promise的结果(resolve或reject时传递的参数值),和传递给 Promise.all 的promise数组的顺序是一致的。

也就是说,这时候 .then 获得的promise数组的执行结果的顺序是固定的,即 [comment, people]。

main().then(function (results) {
    console.log(results); // 按照[comment, people]的顺序
});

若是像下面那样使用一个计时器来计算一下程序执行时间的话,那么就能够很是清楚的知道传递给 Promise.all 的promise数组是同时开始执行的。

promise-all-timer.js
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(delay);
        }, delay);
    });
}
var startDate = Date.now();
// 全部promise变为resolve后程序退出
Promise.all([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(function (values) {
    console.log(Date.now() - startDate + 'ms');
    // 約128ms
    console.log(values);    // [1,32,64,128]
});

timerPromisefy 会每隔必定时间(经过参数指定)以后,返回一个promise对象,状态为FulFilled,其状态值为传给 timerPromisefy的参数。

而传给 Promise.all 的则是由上述promise组成的数组。

var promises = [
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
];

这时候,每隔1, 32, 64, 128 ms都会有一个promise发生 resolve 行为。

也就是说,这个promise对象数组中全部promise都变为resolve状态的话,至少须要128ms。实际咱们计算一下Promise.all 的执行时间的话,它确实是消耗了128ms的时间。

从上述结果能够看出,传递给 Promise.all 的promise并非一个个的顺序执行的,而是同时开始、并行执行的。

若是这些promise所有串行处理的话,那么须要 等待1ms → 等待32ms → 等待64ms → 等待128ms ,所有执行完毕须要225ms的时间。

要想了解更多关于如何使用Promise进行串行处理的内容,能够参考第4章的Promise中的串行处理中的介绍。

2.9. Promise.race

接着咱们来看看和 Promise.all 相似的对多个promise对象进行处理的 Promise.race 方法。

它的使用方法和Promise.all同样,接收一个promise对象数组为参数。

Promise.all 在接收到的全部的对象promise都变为 FulFilled 或者 Rejected 状态以后才会继续进行后面的处理, 与之相对的是 Promise.race 只要有一个promise对象进入 FulFilled 或者 Rejected 状态的话,就会继续进行后面的处理。

像Promise.all时的例子同样,咱们来看一个带计时器的 Promise.race 的使用例子。

promise-race-timer.js
// `delay`毫秒后执行resolve
function timerPromisefy(delay) {
    return new Promise(function (resolve) {
        setTimeout(function () {
            resolve(delay);
        }, delay);
    });
}
// 任何一个promise变为resolve或reject 的话程序就中止运行
Promise.race([
    timerPromisefy(1),
    timerPromisefy(32),
    timerPromisefy(64),
    timerPromisefy(128)
]).then(function (value) {
    console.log(value);    // => 1
});

上面的代码建立了4个promise对象,这些promise对象会分别在1ms,32ms,64ms和128ms后变为肯定状态,即FulFilled,而且在第一个变为肯定状态的1ms后, .then 注册的回调函数就会被调用,这时候肯定状态的promise对象会调用 resolve(1) 所以传递给 value的值也是1,控制台上会打印出1来。

下面咱们再来看看在第一个promise对象变为肯定(FulFilled)状态后,它以后的promise对象是否还在继续运行。

promise-race-other.js
var winnerPromise = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is winner');
            resolve('this is winner');
        }, 4);
    });
var loserPromise = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is loser');
            resolve('this is loser');
        }, 1000);
    });
// 第一个promise变为resolve后程序中止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
    console.log(value);    // => 'this is winner'
});

咱们在前面代码的基础上增长了 console.log 用来输出调试信息。

执行上面代码的话,咱们会看到 winnter和loser promise对象的 setTimeout 方法都会执行完毕, console.log 也会分别输出它们的信息。

也就是说, Promise.race 在第一个promise对象变为Fulfilled以后,并不会取消其余promise对象的执行。

在 ES6 Promises 规范中,也没有取消(中断)promise对象执行的概念,咱们必需要确保promise最终进入resolve or reject状态之一。也就是说Promise并不适用于 状态 可能会固定不变的处理。也有一些类库提供了对promise进行取消的操做。

2.10. then or catch?

在 上一章 里,咱们说过 .catch 也能够理解为 promise.then(undefined, onRejected) 。

在本书里咱们仍是会将 .catch 和 .then 分开使用来进行错误处理的。

此外咱们也会学习一下,在 .then 里同时指定处理对错误进行处理的函数相比,和使用 catch 又有什么异同。

2.10.1. 不能进行错误处理的onRejected

咱们看看下面的这段代码。

then-throw-error.js
function throwError(value) {
    // 抛出异常
    throw new Error(value);
}
// <1> onRejected不会被调用
function badMain(onRejected) {
    return Promise.resolve(42).then(throwError, onRejected);
}
// <2> 有异常发生时onRejected会被调用
function goodMain(onRejected) {
    return Promise.resolve(42).then(throwError).catch(onRejected);
}
// 运行示例
badMain(function(){
    console.log("BAD");
});
goodMain(function(){
    console.log("GOOD");
});

在上面的代码中, badMain 是一个不太好的实现方式(但也不是说它有多坏), goodMain 则是一个能很是好的进行错误处理的版本。

为何说 badMain 很差呢?,由于虽然咱们在 .then 的第二个参数中指定了用来错误处理的函数,但实际上它却不能捕获第一个参数 onFulfilled 指定的函数(本例为 throwError )里面出现的错误。

也就是说,这时候即便 throwError 抛出了异常,onRejected 指定的函数也不会被调用(即不会输出"BAD"字样)。

与此相对的是, goodMain 的代码则遵循了 throwErroronRejected 的调用流程。 这时候 throwError 中出现异常的话,在会被方法链中的下一个方法,即 .catch 所捕获,进行相应的错误处理。

.then 方法中的onRejected参数所指定的回调函数,实际上针对的是其promise对象或者以前的promise对象,而不是针对 .then 方法里面指定的第一个参数,即onFulfilled所指向的对象,这也是 then 和 catch 表现不一样的缘由。

.then 和 .catch 都会建立并返回一个 新的 promise对象。 Promise实际上每次在方法链中增长一次处理的时候所操做的都不是彻底相同的promise对象。

Then Catch flow
Figure 6. Then Catch flow

这种状况下 then 是针对 Promise.resolve(42) 的处理,在onFulfilled 中发生异常,在同一个 then 方法中指定的 onRejected 也不能捕获该异常。

在这个 then 中发生的异常,只有在该方法链后面出现的 catch 方法才能捕获。

固然,因为 .catch 方法是 .then 的别名,咱们使用 .then 也能完成一样的工做。只不过使用 .catch 的话意图更明确,更容易理解。

Promise.resolve(42).then(throwError).then(null, onRejected);

2.10.2. 总结

这里咱们又学习到了以下一些内容。

  1. 使用promise.then(onFulfilled, onRejected) 的话

    • 在 onFulfilled 中发生异常的话,在 onRejected 中是捕获不到这个异常的。

  2. 在 promise.then(onFulfilled).catch(onRejected) 的状况下

    • then 中产生的异常能在 .catch 中捕获

  3. .then 和 .catch 在本质上是没有区别的

    • 须要分场合使用。

咱们须要注意若是代码相似 badMain 那样的话,就可能出现程序不会按预期运行的状况,从而不能正确的进行错误处理。

3. Chapter.3 - Promise测试

这章咱们学习若是编写Promise 的测试代码

3.1. 基本测试

关于ES6 Promises的语法咱们已经学了一些, 我想你们应该也可以在实际项目中编写Promise 的Demo代码了吧。

这时,接下来你可能要苦恼该如何编写Promise 的测试代码了。

那么让咱们先来学习下如何使用 Mocha来对Promise 进行基本的测试吧。

先声明一下,这章中涉及的测试代码都是运行在Node.js环境下的。

本书中出现的示例代码也都有相应的测试代码。 测试代码能够参考 azu/promises-book 。

3.1.1. Mocha

Mocha是Node.js下的测试框架工具,在这里,咱们并不打算对 Mocha自己进行详细讲解。对 Mocha感兴趣的读者能够自行学习。

Mocha能够自由选择BDD、TDD、exports中的任意风格,测试中用到的Assert 方法也一样能够跟任何其余类库组合使用。 也就是说,Mocha自己只提供执行测试时的框架,而其余部分则由使用者本身选择。

这里咱们选择使用Mocha,主要基于下面3点理由。

  • 它是很是著名的测试框架

  • 支持基于Node.js 和浏览器的测试

  • 支持"Promise测试"

最后至于为何说 支持"Promise测试" ,这个咱们在后面再讲。

要想在本章中使用Mocha,咱们须要先经过npm来安装Mocha。

$ npm install -g mocha

另外,Assert库咱们使用的是Node.js自带的assert模块,因此不须要额外安装。

首先,让咱们试着编写一个对传统回调风格的异步函数进行测试的代码。

3.1.2. 回调函数风格的测试

若是想使用回调函数风格来对一个异步处理进行测试,使用Mocha的话代码以下所示。

basic-test.js
var assert = require('power-assert');
describe('Basic Test', function () {
    context('When Callback(high-order function)', function () {
        it('should use `done` for test', function (done) {
            setTimeout(function () {
                assert(true);
                done();
            }, 0);
        });
    });
    context('When promise object', function () {
        it('should use `done` for test?', function (done) {
            var promise = Promise.resolve(1);
            // このテストコードはある欠陥があります
            promise.then(function (value) {
                assert(value === 1);
                done();
            });
        });
    });
});

将这段代码保存为 basic-test.js,以后就能够使用刚才安装的Mocha的命令行工具进行测试了。

$ mocha basic-test.js

Mocha的 it 方法指定了 done 参数,在 done() 函数被执行以前, 该测试一直处于等待状态,这样就能够对异步处理进行测试。

Mocha中的异步测试,将会按照下面的步骤执行。

it("should use `done` for test", function (done) {
    
    setTimeout(function () {
        assert(true);
        done();
    }, 0);
});
回调式的异步处理
调用done 后测试结束

这也是一种很是常见的实现方式。

3.1.3. 使用done 的Promise测试

接下来,让咱们看看如何使用 done 来进行Promise测试。

it("should use `done` for test?", function (done) {
    var promise = Promise.resolve(42);
    promise.then(function (value) {
        assert(value === 42);
        done();
    });
});
建立名为Fulfilled 的promise对象
调用done 后测试结束

Promise.resolve 用来返回promise对象, 返回的promise对象状态为FulFilled。 最后,经过 .then 设置的回调函数也会被调用。

专栏: Promise只能进行异步操做? 中已经提到的那样, promise对象的调用老是异步进行的,因此测试也一样须要以异步调用的方式来编写。

可是,在前面的测试代码中,在assert 失败的状况下就会出现问题。

对异常promise测试
it("should use `done` for test?", function (done) {
    var promise = Promise.resolve();
    promise.then(function (value) {
        assert(false);// => throw AssertionError
        done();
    });
});

在这次测试中 assert 失败了,因此你可能认为应该抛出“测试失败”的错误, 而实际状况倒是测试并不会结束,直到超时。

promise test timeout
Figure 7. 因为测试不会结束,因此直到发生超时时间未知,一直会处于挂起状态。

一般状况下,assert 失败的时候,会throw一个error, 测试框架会捕获该error,来判断测试失败。

可是,Promise的状况下 .then 绑定的函数执行时发生的error 会被Promise捕获,而测试框架则对此error将会一无所知。

咱们来改善一下assert 失败的promise测试, 让它能正确处理 assert 失败时的测试结果。

测试正常失败的示例
it("should use `done` for test?", function (done) {
    var promise = Promise.resolve();
    promise.then(function (value) {
        assert(false);
    }).then(done, done);
});

在上面测试正常失败的示例中,为了确保 done 必定会被调用, 咱们在最后添加了 .then(done, done); 语句。

assert 测试经过(成功)时会调用 done() ,而 assert 失败时则调用 done(error) 。

这样,咱们就编写出了和 回调函数风格的测试 相同的Promise测试。

可是,为了处理 assert 失败的状况,咱们须要额外添加 .then(done, done); 的代码。 这就要求咱们在编写Promise测试时要格外当心,忘了加上上面语句的话,极可能就会写出一个永远不会返回直到超时的测试代码。

在下一节,让咱们接着学习一下最初提到的使用Mocha理由中的支持"Promises测试"到底是一种什么机制。

3.2. Mocha对Promise的支持

在这里,咱们将会学习什么是Mocha支持的“对Promise测试”。

官方网站 Asynchronous code 也记载了关于Promise测试的概要。

Alternately, instead of using the done() callback, you can return a promise. This is useful if the APIs you are testing return promises instead of taking callbacks:

这段话的意思是,在对Promise进行测试的时候,不使用 done() 这样的回调风格的代码编写方式,而是返回一个promise对象。

那么实际上代码将会是什么样的呢?这里咱们来看个具体的例子应该容易理解了。

mocha-promise-test.js
var assert = require('power-assert');
describe('Promise Test', function () {
    it('should return a promise object', function () {
        var promise = Promise.resolve(1);
        return promise.then(function (value) {
            assert(value === 1);
        });
    });
});

这段代码将前面 前面使用 done 的例子 按照Mocha的Promise测试方式进行了重写。

修改的地方主要在如下两点:

  • 删除了 done

  • 返回结果为promise对象

采用这种写法的话,当 assert 失败的时候,测试自己天然也会失败。

it("should be fail", function () {
    return Promise.resolve().then(function () {
        assert(false);// => 测试失败
    });
});

采用这种方法,就能从根本上省略诸如 .then(done, done); 这样本质上跟测试逻辑并没有直接关系的代码。

Mocha已经支持对Promises的测试 | Web scratch 这篇(日语)文章里也提到了关于Mocha对Promise测试的支持。

3.2.1. 意料以外(失败的)的测试结果

由于Mocha提供了对Promise的测试,因此咱们会认为按照Mocha的规则来写会比较好。 可是这种代码可能会带来意想不到的异常状况的发生。

好比对下面的mayBeRejected() 函数的测试代码,该函数返回一个当知足某一条件就变为Rejected的promise对象。

想对Error Object进行测试
function mayBeRejected(){ 
    return Promise.reject(new Error("woo"));
}
it("is bad pattern", function () {
    return mayBeRejected().catch(function (error) {
        assert(error.message === "woo");
    });
});
这个函数用来对返回的promise对象进行测试

这个测试的目的包括如下两点:

mayBeRejected() 返回的promise对象若是变为FulFilled状态的话

测试将会失败

mayBeRejected() 返回的promise对象若是变为Rejected状态的话

在 assert 中对Error对象进行检查

上面的测试代码,当promise对象变为Rejected的时候,会调用在 onRejected 中注册的函数,从而没有走正promise的处理常流程,测试会成功。

这段测试代码的问题在于当mayBeRejected() 返回的是一个 为FulFilled状态的promise对象时,测试会一直成功。

function mayBeRejected(){ 
    return Promise.resolve();
}
it("is bad pattern", function () {
    return mayBeRejected().catch(function (error) {
        assert(error.message === "woo");
    });
});
返回的promise对象会变为FulFilled

在这种状况下,因为在 catch 中注册的 onRejected 函数并不会被调用,所以 assert 也不会被执行,测试会一直经过(passed,成功)。

为了解决这个问题,咱们能够在 .catch 的前面加入一个 .then 调用,能够理解为若是调用了 .then 的话,那么测试就须要失败。

function failTest() { 
    throw new Error("Expected promise to be rejected but it was fulfilled");
}
function mayBeRejected(){
    return Promise.resolve();
}
it("should bad pattern", function () {
    return mayBeRejected().then(failTest).catch(function (error) {
        assert.deepEqual(error.message === "woo");
    });
});
经过throw来使测试失败

可是,这种写法会像在前面 then or catch? 中已经介绍的同样, failTest 抛出的异常会被 catch 捕获。

Then Catch flow
Figure 8. Then Catch flow

程序的执行流程为 then → catch,传递给 catch 的Error对象为AssertionError类型 , 这并非咱们想要的东西。

也就是说,咱们但愿测试只能经过状态会变为onRejected的promise对象, 若是promise对象状态为onFulfilled状态的话,那么该测试就会一直经过。

3.2.2. 明确两种状态,改善测试中的意外(异常)情况

在编写 上面对Error对象进行测试的例子 时, 怎么才能剔除那些会意外经过测试的状况呢?

最简单的方式就是像下面这样,在测试代码中判断在各类promise对象的状态下,应进行如何的操做。

变为FulFilled状态的时候

测试会预期失败

变为Rejected状态的时候

使用 assert 进行测试

也就是说,咱们须要在测试代码中明确指定在Fulfilled和Rejected这两种状态下,都需进行什么样的处理。

function mayBeRejected() {
    return Promise.resolve();
}
it("catch -> then", function () {
    // 变为FulFilled的时候测试失败
    return mayBeRejected().then(failTest, function (error) {
        assert(error.message === "woo");
    });
});

像这样的话,就能在promise变为FulFilled的时候编写出失败用的测试代码了。

Promise onRejected test
Figure 9. Promise onRejected test

在 then or catch? 中咱们已经讲过,为了不遗漏对错误的处理, 与使用 .then(onFulfilled, onRejected) 这样带有二个参数的调用形式相比, 咱们更推荐使用 then → catch 这样的处理方式。

可是在编写测试代码的时候,Promise强大的错误处理机制反而成了限制咱们的障碍。 所以咱们不得已采起了 .then(failTest, onRejected) 这种写法,明确指定promise在各类状态下进行何种的处理。

3.2.3. 总结

在本小节中咱们对在使用Mocha进行Promise测试时可能出现的一些意外状况进行了介绍。

  • 普通的代码采用 then → catch 的流程的话比较容易理解

  • 将测试代码集中到 then 中处理

    • 为了能将AssertionError对象传递到测试框架中。

经过使用 .then(onFulfilled, onRejected) 这种形式的写法, 咱们能够明确指定promise对象在变为 Fulfilled或Rejected时如何进行处理。

可是,因为须要显示的指定 Rejected时的测试处理, 像下面这样的代码看起来老是有一些让人感到不太直观的感受。

promise.then(failTest, function(error){
    // 使用assert对error进行测试
});

在下一小节,咱们会介绍如何编写helper函数以方便编写Promise的测试代码, 以及怎样去编写更容易理解的测试代码。

3.3. 编写可控测试(controllable tests)

在继续进行说明以前,咱们先来定义一下什么是可控测试。在这里咱们对可控测试的定义以下。

待测试的promise对象

  • 若是编写预期为Fulfilled状态的测试的话

    • Rejected的时候要 Fail

    • assertion 的结果不一致的时候要 Fail

  • 若是预期为Rejected状态的话

    • 结果为Fulfilled 测试为 Fail

    • assertion 的结果不一致的时候要 Fail

若是一个测试能网罗上面的用例(Fail)项,那么咱们就称其为可控测试。

也就是说,一个测试用例应该包括下面的测试内容。

  • 结果知足 Fulfilled or Rejected 之一

  • 对传递给assertion的值进行检查

在前面使用了 .then 的代码就是一个指望结果为 Rejected 的测试。

promise.then(failTest, function(error){
    // 经过assert验证error对象
    assert(error instanceof Error);
});

3.3.1. 必须明确指定转换后的状态

为了编写有效的测试代码, 咱们须要明确指定 promise的状态 为 Fulfilled or Rejected 的二者之一。

可是因为 .then 的话在调用的时候能够省略参数,有时候可能会忘记加入使测试失败的条件。

所以,咱们能够定义一个helper函数,用来明肯定义promise指望的状态。

笔者(原著者)建立了一个类库 azu/promise-test-helper 以方便对Promise进行测试,本文中使用的是这个类库的简略版。

首先咱们建立一个名为 shouldRejected 的helper函数,用来在刚才的 .then 的例子中,期待测试返回状态为 onRejected 的结果的例子。

shouldRejected-test.js
var assert = require('power-assert');
function shouldRejected(promise) {
    return {
        'catch': function (fn) {
            return promise.then(function () {
                throw new Error('Expected promise to be rejected but it was fulfilled');
            }, function (reason) {
                fn.call(promise, reason);
            });
        }
    };
}
it('should be rejected', function () {
    var promise = Promise.reject(new Error('human error'));
    return shouldRejected(promise).catch(function (error) {
        assert(error.message === 'human error');
    });
});

shouldRejected 函数接收一个promise对象做为参数,而且返回一个带有 catch 方法的对象。

在这个 catch 中能够使用和 onRejected 里同样的代码,所以咱们能够在 catch 使用基于 assertion 方法的测试代码。

在 shouldRejected 外部,都是相似以下、和普通的promise处理大同小异的代码。

  1. 将须要测试的promise对象传递给 shouldRejected 方法

  2. 在返回的对象的 catch 方法中编写进行onRejected处理的代码

  3. 在onRejected里使用assertion进行判断

在使用 shouldRejected 函数的时候,若是是 Fulfilled 被调用了的话,则会throw一个异常,测试也会失败。

promise.then(failTest, function(error){
    assert(error.message === 'human error');
});
// == 几乎这两段代码是一样的意思
shouldRejected(promise).catch(function (error) {
    assert(error.message === 'human error');
});

使用 shouldRejected 这样的helper函数,测试代码也会变得很直观。

Promise onRejected test
Figure 10. Promise onRejected test

像上面同样,咱们也能够编写一个测试promise对象期待结果为Fulfilled的 shouldFulfilled helper函数。

shouldFulfilled-test.js
var assert = require('power-assert');
function shouldFulfilled(promise) {
    return {
        'then': function (fn) {
            return promise.then(function (value) {
                fn.call(promise, value);
            }, function (reason) {
                throw reason;
            });
        }
    };
}
it('should be fulfilled', function () {
    var promise = Promise.resolve('value');
    return shouldFulfilled(promise).then(function (value) {
        assert(value === 'value');
    });
});

这和上面的 shouldRejected-test.js 结构基本相同,只不过返回对象的 catch 方法变为了 then ,promise.then的两个参数也调换了。

3.3.2. 小结

在本小节咱们学习了如何编写针对Promise特定状态的测试代码,以及如何使用便于测试的helper函数。

这里咱们使用到的 shouldFulfilled 和 shouldRejected 也能够在下面的类库中找到。

此外,本小节中的helper方法都是以 Mocha对Promise的支持 为前提的, 在 基于done 的测试 中使用的话可能会比较麻烦。

是使用基于测试框架对Promis的支持,仍是使用基于相似done 这样回调风格的测试方式,每一个人均可以自由的选择,只是风格问题,我以为倒不必去争一个孰优孰劣。

好比在 CoffeeScript下进行测试的话,因为CoffeeScript 会隐式的使用return返回,因此使用 done 的话可能更容易理解一些。

对Promise进行测试比对一般的异步函数进行测试坑更多,虽然说采起什么样的测试方法是我的的自由,可是在同一项目中采起先后风格一致的测试则是很是重要。

4. Chapter.4 - Advanced

在这一章里,咱们会基于前面学到的内容,再深刻了解一下Promise里的一些高级内容,加深对Promise的理解。

4.1. Promise的实现类库(Library)

在本小节里,咱们将不打算对浏览器实现的Promise进行说明,而是要介绍一些第三方实现的和Promise兼容的类库。

4.1.1. 为何须要这些类库?

为何须要这些类库呢?我想有些读者难免会有此疑问。首先能想到的缘由是有些运行环境并不支持 ES6 Promises 。

当咱们在网上查找Promise的实现类库的时候,有一个因素是首先要考虑的,那就是是否具备 Promises/A+兼容性 。

Promises/A+ 是 ES6 Promises 的前身,Promise的 then 也是来自于此的基于社区的规范。

若是说一个类库兼容 Promises/A+ 的话,那么就是说它除了具备标准的 then 方法以外,不少状况下也说明此类库还支持 Promise.all和 catch 等功能。

可是 Promises/A+ 实际上只是定义了关于 Promise#then 的规范,因此有些类库可能实现了其它诸如 all 或 catch 等功能,可是可能名字却不同。

若是咱们说一个类库具备 then 兼容性的话,实际上指的是 Thenable ,它经过使用 Promise.resolve 基于ES6 Promise的规定,进行promise对象的变换。

ES6 Promise 里关于promise对象的规定包括在使用 catch 方法,或使用 Promise.all 进行处理的时候不能出现错误。

4.1.2. Polyfill和扩展类库

在这些Promise的实现类库中,咱们这里主要对两种类型的类库进行介绍。

一种是被称为 Polyfill (这是一款英国产品,就是装修刮墙用的腻子,其意义可想而知 — 译者注)的类库,另外一种是即具备 Promises/A+兼容性 ,又增长了本身独特功能的类库。

Promise的实现类库数量很是之多,这里咱们只是介绍了其中有限的几个。
Polyfill

只须要在浏览器中加载Polyfill类库,就能使用IE10等或者尚未提供对Promise支持的浏览器中使用Promise里规定的方法。

也就是说若是加载了Polyfill类库,就能在还不支持Promise的环境中,运行本文中的各类示例代码。

jakearchibald/es6-promise

一个兼容 ES6 Promises 的Polyfill类库。 它基于 RSVP.js 这个兼容 Promises/A+ 的类库, 它只是 RSVP.js 的一个子集,只实现了Promises 规定的 API。

yahoo/ypromise

这是一个独立版本的 YUI 的 Promise Polyfill,具备和 ES6 Promises 的兼容性。 本书的示例代码也都是基于这个 ypromise 的 Polyfill 来在线运行的。

getify/native-promise-only

以做为ES6 Promises的polyfill为目的的类库 它严格按照ES6 Promises的规范设计,没有添加在规范中没有定义的功能。 若是运行环境有原生的Promise支持的话,则优先使用原生的Promise支持。

Promise扩展类库

Promise扩展类库除了实现了Promise中定义的规范以外,还增长了本身独自定义的功能。

Promise扩展类库数量很是的多,咱们只介绍其中两个比较有名的类库。

kriskowal/q

类库 Q 实现了 Promises 和 Deferreds 等规范。 它自2009年开始开发,还提供了面向Node.js的文件IO API Q-IO 等, 是一个在不少场景下都能用获得的类库。

petkaantonov/bluebird

这个类库除了兼容 Promise 规范以外,还扩展了取消promise对象的运行,取得promise的运行进度,以及错误处理的扩展检测等很是丰富的功能,此外它在实现上还在性能问题下了很大的功夫。

Q 和 Bluebird 这两个类库除了都能在浏览器里运行以外,充实的API reference也是其特征。

Q等文档里详细介绍了Q的Deferred和jQuery里的Deferred有哪些异同,以及要怎么进行迁移 Coming from jQuery 等都进行了详细的说明。

Bluebird的文档除了提供了使用Promise丰富的实现方式以外,还涉及到了在出现错误时的对应方法以及 Promise中的反模式 等内容。

这两个类库的文档写得都很友好,即便咱们不使用这两个类库,阅读一下它们的文档也具备必定的参考价值。

4.1.3. 总结

本小节介绍了Promise的实现类库中的 Polyfill 和扩展类库这两种。

Promise的实现类库种类繁多,到底选择哪一个来使用彻底看本身的喜爱了。

可是因为这些类库实现的 Promise 同时具备 Promises/A+ 或 ES6 Promises 共通的接口,因此在使用某一类库的时候,有时候也能够参考一下其余类库的代码或者扩展功能。

熟练掌握Promise中的共通概念,进而能在实际中能对这些技术运用自如,这也是本书的写做目的之一。

4.2. Promise.resolve和Thenable

在 第二章的Promise.resolve 中咱们已经说过, Promise.resolve 的最大特征之一就是能够将thenable的对象转换为promise对象。

在本小节里,咱们将学习一下利用将thenable对象转换为promise对象这个功能都能具体作些什么事情。

4.2.1. 将Web Notifications转换为thenable对象

这里咱们以桌面通知 API Web Notifications 为例进行说明。

关于Web Notifications API的详细信息能够参考下面的网址。

简单来讲,Web Notifications API就是能像如下代码那样经过 new Notification 来显示通知消息。

new Notification("Hi!");

固然,为了显示通知消息,咱们须要在运行 new Notification 以前,先得到用户的许可。

确认是否容许Notification的对话框
Figure 11. 确认是否容许Notification的对话框

用户在这个是否容许Notification的对话框选择后的结果,会经过 Notification.permission 传给咱们的程序,它的值多是容许("granted")或拒绝("denied")这两者之一。

是否容许Notification对话框中的可选项,在Firefox中除了容许、拒绝以外,还增长了 永久有效 和 会话范围内有效 两种额外选项,固然 Notification.permission 的值都是同样的。

在程序中能够经过 Notification.requestPermission() 来弹出是否容许Notification对话框, 用户选择的结果会经过 status 参数传给回调函数。

从这个回调函数咱们也能够看出来,用户选择容许仍是拒绝通知是异步进行的。

Notification.requestPermission(function (status) {
    // status的值为 "granted" 或 "denied"
    console.log(status);
});

到用户收到并显示通知为止,总体的处理流程以下所示。

  • 显示是否容许通知的对话框,并异步处理用户选择结果

  • 若是用户容许的话,则经过 new Notification 显示通知消息。这又分两种状况

    • 用户以前已经容许过

    • 当场弹出是否容许桌面通知对话框

  • 当用户不容许的时候,不执行任何操做

虽然上面说到了几种情景,可是最终结果就是用户容许或者拒绝,能够总结为以下两种模式。

容许时("granted")

使用 new Notification 建立通知消息

拒绝时("denied")

没有任何操做

这两种模式是否是以为有在哪里看过的感受? 呵呵,用户的选择结果,正和在Promise中promise对象变为 Fulfilled 或 Rejected 状态很是相似。

resolve(成功)时 == 用户容许("granted")

调用 onFulfilled 方法

reject(失败)时 == 用户拒绝("denied")

调用 onRejected 函数

是否是咱们能够用Promise的方式去编写桌面通知的代码呢?咱们先从回调函数风格的代码入手看看到底怎么去作。

4.2.2. Web Notification 包装函数(wrapper)

首先,咱们以回到函数风格的代码对上面的Web Notification API包装函数进行重写,新代码以下所示。

notification-callback.js
function notifyMessage(message, options, callback) {
    if (Notification && Notification.permission === 'granted') {
        var notification = new Notification(message, options);
        callback(null, notification);
    } else if (Notification.requestPermission) {
        Notification.requestPermission(function (status) {
            if (Notification.permission !== status) {
                Notification.permission = status;
            }
            if (status === 'granted') {
                var notification = new Notification(message, options);
                callback(null, notification);
            } else {
                callback(new Error('user denied'));
            }
        });
    } else {
        callback(new Error('doesn\'t support Notification API'));
    }
}
// 运行实例
// 第二个参数是传给 `Notification` 的option对象
notifyMessage("Hi!", {}, function (error, notification) {
    if(error){
        return console.error(error);
    }
    console.log(notification);// 通知对象
});

在回调风格的代码里,当用户拒绝接收通知的时候, error 会被设置值,而若是用户赞成接收通知的时候,则会显示通知消息而且 notification 会被设置值。

回调函数接收error和notification两个参数
function callback(error, notification){

}

下面,我想再将这个回调函数风格的代码使用Promise进行改写。

4.2.3. Web Notification as Promise

基于上述回调风格的 notifyMessage 函数,咱们再来建立一个返回promise对象的 notifyMessageAsPromise 方法。

notification-as-promise.js
function notifyMessage(message, options, callback) {
    if (Notification && Notification.permission === 'granted') {
        var notification = new Notification(message, options);
        callback(null, notification);
    } else if (Notification.requestPermission) {
        Notification.requestPermission(function (status) {
            if (Notification.permission !== status) {
                Notification.permission = status;
            }
            if (status === 'granted') {
                var notification = new Notification(message, options);
                callback(null, notification);
            } else {
                callback(new Error('user denied'));
            }
        });
    } else {
        callback(new Error('doesn\'t support Notification API'));
    }
}
function notifyMessageAsPromise(message, options) {
    return new Promise(function (resolve, reject) {
        notifyMessage(message, options, function (error, notification) {
            if (error) {
                reject(error);
            } else {
                resolve(notification);
            }
        });
    });
}
// 运行示例
notifyMessageAsPromise("Hi!").then(function (notification) {
    console.log(notification);// 通知对象
}).catch(function(error){
    console.error(error);
});

在用户容许接收通知的时候,运行上面的代码,会显示 "Hi!" 消息。

当用户接收通知消息的时候, .then 函数会被调用,当用户拒绝接收消息的时候, .catch 方法会被调用。

因为浏览器是以网站为单位保存Web Notifications API的许可状态的,因此实际上有下面四种模式存在。

已经得到用户许可

.then 方法被调用

弹出询问对话框并得到许可

.then 方法被调用

已是被用户拒绝的状态

.catch 方法被调用

弹出询问对话框并被用户拒绝

.catch 方法被调用

也就是说,若是使用原生的Web Notifications API的话,那么须要在程序中对上述四种状况都进行处理,咱们能够像下面的包装函数那样,将上述四种状况简化为两种以方便处理。

上面的 notification-as-promise.js 虽然看上去很方便,可是实际上使用的时候,极可能出现 在不支持Promise的环境下不能使用 的问题。

若是你想编写像notification-as-promise.js这样具备Promise风格和的类库的话,我以为你有以下的一些选择。

支持Promise的环境是前提
  • 须要最终用户保证支持Promise

  • 在不支持Promise的环境下不能正常工做(即应该出错)。

在类库中实现 Promise
  • 在类库中实现Promise功能

  • 例如) localForage

在回调函数中也应该可以使用  Promise
  • 用户能够选择合适的使用方式

  • 返回Thenable类型

notification-as-promise.js就是以Promise存在为前提的写法。

回归正文,在这里Thenable是为了帮助实现在回调函数中也能使用Promise的一个概念。

4.2.4. Web Notifications As Thenable

咱们已经说过,thenable就是一个具备 .then方法的一个对象。下面咱们就在notification-callback.js中增长一个返回值为 thenable 类型的方法。

notification-thenable.js
function notifyMessage(message, options, callback) {
    if (Notification && Notification.permission === 'granted') {
        var notification = new Notification(message, options);
        callback(null, notification);
    } else if (Notification.requestPermission) {
        Notification.requestPermission(function (status) {
            if (Notification.permission !== status) {
                Notification.permission = status;
            }
            if (status === 'granted') {
                var notification = new Notification(message, options);
                callback(null, notification);
            } else {
                callback(new Error('user denied'));
            }
        });
    } else {
        callback(new Error('doesn\'t support Notification API'));
    }
}
// 返回 `thenable`
function notifyMessageAsThenable(message, options) {
    return {
        'then': function (resolve, reject) {
            notifyMessage(message, options, function (error, notification) {
                if (error) {
                    reject(error);
                } else {
                    resolve(notification);
                }
            });
        }
    };
}
// 运行示例
Promise.resolve(notifyMessageAsThenable("message")).then(function (notification) {
    console.log(notification);// 通知对象
}).catch(function(error){
    console.error(error);
});

notification-thenable.js里增长了一个 notifyMessageAsThenable方法。这个方法返回的对象具有一个then方法。

then方法的参数和 new Promise(function (resolve, reject){}) 同样,在肯定时执行 resolve 方法,拒绝时调用 reject 方法。

then 方法和 notification-as-promise.js 中的 notifyMessageAsPromise 方法完成了一样的工做。

咱们能够看出, Promise.resolve(thenable) 经过使用了 thenable 这个promise对象,就能利用Promise功能了。

Promise.resolve(notifyMessageAsThenable("message")).then(function (notification) {
    console.log(notification);// 通知对象
}).catch(function(error){
    console.error(error);
});

使用了Thenable的notification-thenable.js 和依赖于Promise的 notification-as-promise.js ,实际上都是很是类似的使用方法。

notification-thenable.js 和 notification-as-promise.js比起来,有如下的不一样点。

  • 类库侧没有提供 Promise 的实现

    • 用户经过 Promise.resolve(thenable) 来本身实现了 Promise

  • 做为Promise使用的时候,须要和 Promise.resolve(thenable) 一块儿配合使用

经过使用Thenable对象,咱们能够实现相似已有的回调式风格和Promise风格中间的一种实现风格。

4.2.5. 总结

在本小节咱们主要学习了什么是Thenable,以及如何经过Promise.resolve(thenable) 使用Thenable,将其做为promise对象来使用。

Callback — Thenable — Promise

Thenable风格表现为位于回调和Promise风格中间的一种状态,做为类库的公开API有点不太成熟,因此并不常见。

Thenable自己并不依赖于Promise功能,可是Promise以外也没有使用Thenable的方式,因此能够认为Thenable间接依赖于Promise。

另外,用户须要对 Promise.resolve(thenable) 有所理解才能使用好Thenable,所以做为类库的公开API有一部分会比较难。和公开API相比,更多状况下是在内部使用Thenable。

在编写异步处理的类库的时候,推荐采用先编写回调风格的函数,而后再转换为公开API这种方式。

貌似Node.js的Core module就采用了这种方式,除了类库提供的基本回调风格的函数以外,用户也能够经过Promise或者Generator等本身擅长的方式进行实现。

最初就是以能被Promise使用为目的的类库,或者其自己依赖于Promise等状况下,我想将返回promise对象的函数做为公开API应该也没什么问题。

何时该使用Thenable?

那么,又是在什么状况下应该使用Thenable呢?

恐怕最可能被使用的是在 Promise类库 之间进行相互转换了。

好比,类库Q的Promise实例为Q promise对象,提供了 ES6 Promises 的promise对象不具有的方法。Q promise对象提供了 promise.finally(callback) 和 promise.nodeify(callback) 等方法。

若是你想将ES6 Promises的promise对象转换为Q promise的对象,轮到Thenable大显身手的时候就到了。

使用thenable将promise对象转换为Q promise对象
var Q = require("Q");
// 这是一个ES6的promise对象
var promise = new Promise(function(resolve){
    resolve(1);
});
// 变换为Q promise对象
Q(promise).then(function(value){
    console.log(value);
}).finally(function(){ 
    console.log("finally");
});
由于是Q promise对象因此能够使用 finally 方法

上面代码中最开始被建立的promise对象具有then方法,所以是一个Thenable对象。咱们能够经过Q(thenable)方法,将这个Thenable对象转换为Q promise对象。

能够说它的机制和 Promise.resolve(thenable) 同样,固然反过来也同样。

像这样,Promise类库虽然都有本身类型的promise对象,可是它们之间能够经过Thenable这个共通概念,在类库之间(固然也包括native Promise)进行promise对象的相互转换。

咱们看到,就像上面那样,Thenable多在类库内部实现中使用,因此从外部来讲不会常常看到Thenable的使用。可是咱们必须牢记Thenable是Promise中一个很是重要的概念。

4.3. 使用reject而不是throw

Promise的构造函数,以及被 then 调用执行的函数基本上均可以认为是在 try...catch 代码块中执行的,因此在这些代码中即便使用 throw ,程序自己也不会由于异常而终止。

若是在Promise中使用 throw 语句的话,会被 try...catch 住,最终promise对象也变为Rejected状态。

var promise = new Promise(function(resolve, reject){
    throw new Error("message");
});
promise.catch(function(error){
    console.error(error);// => "message"
});

代码像这样其实运行时倒也不会有什么问题,可是若是想把 promise对象状态 设置为Rejected状态的话,使用 reject 方法则更显得合理。

因此上面的代码能够改写为下面这样。

var promise = new Promise(function(resolve, reject){
    reject(new Error("message"));
});
promise.catch(function(error){
    console.error(error);// => "message"
})

其实咱们也能够这么来考虑,在出错的时候咱们并无调用 throw 方法,而是使用了 reject ,那么给 reject 方法传递一个Error类型的对象也就很好理解了。

4.3.1. 使用reject有什么优势?

话说回来,为何在想将promise对象的状态设置为Rejected的时候应该使用 reject 而不是 throw 呢?

首先是由于咱们很难区分 throw 是咱们主动抛出来的,仍是由于真正的其它 异常 致使的。

好比在使用Chrome浏览器的时候,Chrome的开发者工具提供了在程序发生异常的时候自动在调试器中break的功能。

Pause On Caught Exceptions
Figure 12. Pause On Caught Exceptions

当咱们开启这个功能的时候,在执行到下面代码中的 throw 时就会触发调试器的break行为。

var promise = new Promise(function(resolve, reject){
    throw new Error("message");
});

原本这是和调试没有关系的地方,也由于在Promise中的 throw 语句被break了,这也严重的影响了浏览器提供的此功能的正常使用。

4.3.2. 在then中进行reject

在Promise构造函数中,有一个用来指定 reject 方法的参数,使用这个参数而不是依靠 throw 将promise对象的状态设置为Rejected状态很是简单。

那么若是像下面那样想在 then 中进行reject的话该怎么办呢?

var promise = Promise.resolve();
promise.then(function (value) {
    setTimeout(function () {
        // 通过一段时间后还没处理完的话就进行reject - 2
    }, 1000);
    // 比较耗时的处理 - 1
    somethingHardWork();
}).catch(function (error) {
    // 超时错误 - 3
});

上面的超时处理,须要在 then 中进行 reject 方法调用,可是传递给当前的回调函数的参数只有前面的一promise对象,这该怎么办呢?

关于使用Promise进行超时处理的具体实现方法能够参考 使用Promise.race和delay取消XHR请求 中的详细说明。

在这里咱们再次回忆下 then 的工做原理。

在 then 中注册的回调函数能够经过 return 返回一个值,这个返回值会传给后面的 then 或 catch 中的回调函数。

并且return的返回值类型不光是简单的字面值,还能够是复杂的对象类型,好比promise对象等。

这时候,若是返回的是promise对象的话,那么根据这个promise对象的状态,在下一个 then 中注册的回调函数中的onFulfilled和onRejected的哪个会被调用也是能肯定的。

var promise = Promise.resolve();
promise.then(function () {
    var retPromise = new Promise(function (resolve, reject) {
        // resolve or reject 的状态决定 onFulfilled or onRejected 的哪一个方法会被调用
    });
    return retPromise;
}).then(onFulfilled, onRejected);
后面的then调用哪一个回调函数是由promise对象的状态来决定的

也就是说,这个 retPromise 对象状态为Rejected的时候,会调用后面then中的 onRejected 方法,这样就实现了即便在 then 中不使用 throw 也能进行reject处理了。

var onRejected = console.error.bind(console);
var promise = Promise.resolve();
promise.then(function () {
    var retPromise = new Promise(function (resolve, reject) {
       reject(new Error("this promise is rejected"));
    });
    return retPromise;
}).catch(onRejected);

使用 Promise.reject 的话还能再将代码进行简化。

var onRejected = console.error.bind(console);
var promise = Promise.resolve();
promise.then(function () {
    return Promise.reject(new Error("this promise is rejected"));
}).catch(onRejected);

4.3.3. 总结

在本小节咱们主要学习了

  • 使用 reject 会比使用 throw 安全

  • 在 then 中使用reject的方法

也许实际中咱们可能不常使用 reject ,可是比起来不假思索的使用 throw 来讲,使用 reject 的好处仍是不少的。

关于上面讲的内容的比较详细的例子,你们能够参考在 使用Promise.race和delay取消XHR请求 小节的介绍。

4.4. Deferred和Promise

这一节咱们来简单介绍下Deferred和Promise之间的关系

4.4.1. 什么是Deferred?

提及Promise,我想你们必定同时也据说过Deferred这个术语。好比 jQuery.Deferred 和 JSDeferred 等,必定都是你们很是熟悉的内容了。

Deferred和Promise不一样,它没有共通的规范,每一个Library都是根据本身的喜爱来实现的。

在这里,咱们打算以 jQuery.Deferred 相似的实现为中心进行介绍。

4.4.2. Deferred和Promise的关系

简单来讲,Deferred和Promise具备以下的关系。

  • Deferred 拥有 Promise

  • Deferred 具有对 Promise的状态进行操做的特权方法(图中的"特権メソッド")

Deferred和Promise
Figure 13. Deferred和Promise

我想各位看到此图应该就很容易理解了,Deferred和Promise并非处于竞争的关系,而是Deferred内涵了Promise。

这是jQuery.Deferred结构的简化版。固然也有的Deferred实现并无内涵Promise。

光看图的话也许还难以理解,下面咱们就看看看怎么经过Promise来实现Deferred。

4.4.3. Deferred top on Promise

基于Promise实现Deferred的例子。

deferred.js
function Deferred() {
    this.promise = new Promise(function (resolve, reject) {
        this._resolve = resolve;
        this._reject = reject;
    }.bind(this));
}
Deferred.prototype.resolve = function (value) {
    this._resolve.call(this.promise, value);
};
Deferred.prototype.reject = function (reason) {
    this._reject.call(this.promise, reason);
};

咱们再将以前使用Promise实现的 getURL 用Deferred改写一下。

xhr-deferred.js
function Deferred() {
    this.promise = new Promise(function (resolve, reject) {
        this._resolve = resolve;
        this._reject = reject;
    }.bind(this));
}
Deferred.prototype.resolve = function (value) {
    this._resolve.call(this.promise, value);
};
Deferred.prototype.reject = function (reason) {
    this._reject.call(this.promise, reason);
};
function getURL(URL) {
    var deferred = new Deferred();
    var req = new XMLHttpRequest();
    req.open('GET', URL, true);
    req.onload = function () {
        if (req.status === 200) {
            deferred.resolve(req.responseText);
        } else {
            deferred.reject(new Error(req.statusText));
        }
    };
    req.onerror = function () {
        deferred.reject(new Error(req.statusText));
    };
    req.send();
    return deferred.promise;
}
// 运行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
    console.log(value);
}).catch(console.error.bind(console));

所谓的能对Promise状态进行操做的特权方法,指的就是能对promise对象的状态进行resolve、reject等调用的方法,而一般的Promise的话只能在经过构造函数传递的方法以内对promise对象的状态进行操做。

咱们来看看Deferred和Promise相比在实现上有什么异同。

xhr-promise.js
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
// 运行示例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
    console.log(value);
}).catch(console.error.bind(console));

对比上述两个版本的 getURL ,咱们发现它们有以下不一样。

  • Deferred 的话不须要将代码用Promise括起来

    • 因为没有被嵌套在函数中,能够减小一层缩进

    • 反过来没有Promise里的错误处理逻辑

在如下方面,它们则完成了一样的工做。

  • 总体处理流程

    • 调用 resolvereject 的时机

  • 函数都返回了promise对象

因为Deferred包含了Promise,因此大致的流程仍是差很少的,不过Deferred有用对Promise进行操做的特权方法,以及高度自由的对流程控制进行自由定制。

好比在Promise通常都会在构造函数中编写主要处理逻辑,对 resolvereject 方法的调用时机也基本是很肯定的。

new Promise(function (resolve, reject){
    // 在这里进行promise对象的状态肯定
});

而使用Deferred的话,并不须要将处理逻辑写成一大块代码,只须要先建立deferred对象,能够在任什么时候机对 resolvereject 方法进行调用。

var deferred = new Deferred();

// 能够在随意的时机对 `resolve`、`reject` 方法进行调用

上面咱们只是简单的实现了一个 Deferred ,我想你已经看到了它和 Promise 之间的差别了吧。

若是说Promise是用来对值进行抽象的话,Deferred则是对处理尚未结束的状态或操做进行抽象化的对象,咱们也能够从这一层的区别来理解一下这二者之间的差别。

换句话说,Promise表明了一个对象,这个对象的状态如今还不肯定,可是将来一个时间点它的状态要么变为正常值(FulFilled),要么变为异常值(Rejected);而Deferred对象表示了一个处理尚未结束的这种事实,在它的处理结束的时候,能够经过Promise来取得处理结果。

若是各位读者还想深刻了解一下Deferred的话,能够参考下面的这些资料。

Deferred最初是在Python的 Twisted 框架中被提出来的概念。 在JavaScript领域能够认为它是由 MochiKit.Async 、 dojo/Deferred 等Library引入的。

4.5. 使用Promise.race和delay取消XHR请求

在本小节中,做为在第2章所学的 Promise.race 的具体例子,咱们来看一下如何使用Promise.race来实现超时机制。

固然XHR有一个 timeout 属性,使用该属性也能够简单实现超时功能,可是为了能支持多个XHR同时超时或者其余功能,咱们采用了容易理解的异步方式在XHR中经过超时来实现取消正在进行中的操做。

4.5.1. 让Promise等待指定时间

首先咱们来看一下如何在Promise中实现超时。

所谓超时就是要在通过必定时间后进行某些操做,使用 setTimeout 的话很好理解。

首先咱们来串讲一个单纯的在Promise中调用 setTimeout 的函数。

delayPromise.js
function delayPromise(ms) {
    return new Promise(function (resolve) {
        setTimeout(resolve, ms);
    });
}

delayPromise(ms) 返回一个在通过了参数指定的毫秒数后进行onFulfilled操做的promise对象,这和直接使用 setTimeout 函数比较起来只是编码上略有不一样,以下所示。

setTimeout(function () {
    alert("已通过了100ms!");
}, 100);
// == 几乎一样的操做
delayPromise(100).then(function () {
    alert("已通过了100ms!");
});

在这里 promise对象 这个概念很是重要,请切记。

4.5.2. Promise.race中的超时

让咱们回顾一下静态方法 Promise.race ,它的做用是在任何一个promise对象进入到肯定(解决)状态后就继续进行后续处理,以下面的例子所示。

var winnerPromise = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is winner');
            resolve('this is winner');
        }, 4);
    });
var loserPromise = new Promise(function (resolve) {
        setTimeout(function () {
            console.log('this is loser');
            resolve('this is loser');
        }, 1000);
    });
// 第一个promise变为resolve后程序中止
Promise.race([winnerPromise, loserPromise]).then(function (value) {
    console.log(value);    // => 'this is winner'
});

咱们能够将刚才的 delayPromise 和其它promise对象一块儿放到 Promise.race 中来是实现简单的超时机制。

simple-timeout-promise.js
function delayPromise(ms) {
    return new Promise(function (resolve) {
        setTimeout(resolve, ms);
    });
}
function timeoutPromise(promise, ms) {
    var timeout = delayPromise(ms).then(function () {
            throw new Error('Operation timed out after ' + ms + ' ms');
        });
    return Promise.race([promise, timeout]);
}

函数 timeoutPromise(比较对象promise, ms) 接收两个参数,第一个是须要使用超时机制的promise对象,第二个参数是超时时间,它返回一个由 Promise.race 建立的相互竞争的promise对象。

以后咱们就能够使用 timeoutPromise 编写下面这样的具备超时机制的代码了。

function delayPromise(ms) {
    return new Promise(function (resolve) {
        setTimeout(resolve, ms);
    });
}
function timeoutPromise(promise, ms) {
    var timeout = delayPromise(ms).then(function () {
            throw new Error('Operation timed out after ' + ms + ' ms');
        });
    return Promise.race([promise, timeout]);
}
// 运行示例
var taskPromise = new Promise(function(resolve){
    // 随便一些什么处理
    var delay = Math.random() * 2000;
    setTimeout(function(){
        resolve(delay + "ms");
    }, delay);
});
timeoutPromise(taskPromise, 1000).then(function(value){
    console.log("taskPromise在规定时间内结束 : " + value);
}).catch(function(error){
    console.log("发生超时", error);
});

虽然在发生超时的时候抛出了异常,可是这样的话咱们就不能区分这个异常究竟是普通的错误仍是超时错误了。

为了能区分这个 Error 对象的类型,咱们再来定义一个Error 对象的子类 TimeoutError

4.5.3. 定制Error对象

Error 对象是ECMAScript的内建(build in)对象。

可是因为stack trace等缘由咱们不能完美的建立一个继承自 Error 的类,不过在这里咱们的目的只是为了和Error有所区别,咱们将建立一个 TimeoutError 类来实现咱们的目的。

在ECMAScript6中能够使用 class 语法来定义类之间的继承关系。

class MyError extends Error{
    // 继承了Error类的对象
}

为了让咱们的 TimeoutError 能支持相似 error instanceof TimeoutError 的使用方法,咱们还须要进行以下工做。

TimeoutError.js
function copyOwnFrom(target, source) {
    Object.getOwnPropertyNames(source).forEach(function (propName) {
        Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
    });
    return target;
}
function TimeoutError() {
    var superInstance = Error.apply(null, arguments);
    copyOwnFrom(this, superInstance);
}
TimeoutError.prototype = Object.create(Error.prototype);
TimeoutError.prototype.constructor = TimeoutError;

咱们定义了 TimeoutError 类和构造函数,这个类继承了Error的prototype。

它的使用方法和普通的 Error 对象同样,使用 throw 语句便可,以下所示。

var promise = new Promise(function(){
    throw TimeoutError("timeout");
});

promise.catch(function(error){
    console.log(error instanceof TimeoutError);// true
});

有了这个 TimeoutError 对象,咱们就能很容易区分捕获的究竟是由于超时而致使的错误,仍是其余缘由致使的Error对象了。

本章里介绍的继承JavaScript内建对象的方法能够参考 Chapter 28. Subclassing Built-ins ,那里有详细的说明。此外 Error - JavaScript | MDN 也针对Error对象进行了详细说明。

4.5.4. 经过超时取消XHR操做

到这里,我想各位读者都已经对如何使用Promise来取消一个XHR请求都有一些思路了吧。

取消XHR操做自己的话并不难,只须要调用 XMLHttpRequest 对象的 abort() 方法就能够了。

为了能在外部调用 abort() 方法,咱们先对以前本节出现的 getURL 进行简单的扩展,cancelableXHR 方法除了返回一个包装了XHR的promise对象以外,还返回了一个用于取消该XHR请求的abort方法。

delay-race-cancel.js
function cancelableXHR(URL) {
    var req = new XMLHttpRequest();
    var promise = new Promise(function (resolve, reject) {
            req.open('GET', URL, true);
            req.onload = function () {
                if (req.status === 200) {
                    resolve(req.responseText);
                } else {
                    reject(new Error(req.statusText));
                }
            };
            req.onerror = function () {
                reject(new Error(req.statusText));
            };
            req.onabort = function () {
                reject(new Error('abort this request'));
            };
            req.send();
        });
    var abort = function () {
        // 若是request尚未结束的话就执行abort
        // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
        if (req.readyState !== XMLHttpRequest.UNSENT) {
            req.abort();
        }
    };
    return {
        promise: promise,
        abort: abort
    };
}

在这些问题都明了以后,剩下只须要进行Promise处理的流程进行编码便可。大致的流程就像下面这样。

  1. 经过 cancelableXHR 方法取得包装了XHR的promise对象和取消该XHR请求的方法

  2. 在 timeoutPromise 方法中经过 Promise.race 让XHR的包装promise和超时用promise进行竞争。

    • XHR在超时前返回结果的话

      1. 和正常的promise同样,经过 then 返回请求结果

    • 发生超时的时候

      1. 抛出 throw TimeoutError 异常并被 catch

      2. catch的错误对象若是是 TimeoutError 类型的话,则调用 abort 方法取消XHR请求

将上面的步骤总结一下的话,代码以下所示。

delay-race-cancel-play.js
function copyOwnFrom(target, source) {
    Object.getOwnPropertyNames(source).forEach(function (propName) {
        Object.defineProperty(target, propName, Object.getOwnPropertyDescriptor(source, propName));
    });
    return target;
}
function TimeoutError() {
    var superInstance = Error.apply(null, arguments);
    copyOwnFrom(this, superInstance);
}
TimeoutError.prototype = Object.create(Error.prototype);
TimeoutError.prototype.constructor = TimeoutError;
function delayPromise(ms) {
    return new Promise(function (resolve) {
        setTimeout(resolve, ms);
    });
}
function timeoutPromise(promise, ms) {
    var timeout = delayPromise(ms).then(function () {
            return Promise.reject(new TimeoutError('Operation timed out after ' + ms + ' ms'));
        });
    return Promise.race([promise, timeout]);
}
function cancelableXHR(URL) {
    var req = new XMLHttpRequest();
    var promise = new Promise(function (resolve, reject) {
            req.open('GET', URL, true);
            req.onload = function () {
                if (req.status === 200) {
                    resolve(req.responseText);
                } else {
                    reject(new Error(req.statusText));
                }
            };
            req.onerror = function () {
                reject(new Error(req.statusText));
            };
            req.onabort = function () {
                reject(new Error('abort this request'));
            };
            req.send();
        });
    var abort = function () {
        // 若是request尚未结束的话就执行abort
        // https://developer.mozilla.org/en/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest
        if (req.readyState !== XMLHttpRequest.UNSENT) {
            req.abort();
        }
    };
    return {
        promise: promise,
        abort: abort
    };
}
var object = cancelableXHR('http://httpbin.org/get');
// main
timeoutPromise(object.promise, 1000).then(function (contents) {
    console.log('Contents', contents);
}).catch(function (error) {
    if (error instanceof TimeoutError) {
        object.abort();
        return console.log(error);
    }
    console.log('XHR Error :', error);
});

上面的代码就经过在必定的时间内变为解决状态的promise对象实现了超时处理。

一般进行开发的状况下,因为这些逻辑会频繁使用,所以将这些代码分割保存在不一样的文件应该是一个不错的选择。

4.5.5. promise和操做方法

在前面的 cancelableXHR 中,promise对象及其操做方法都是在一个对象中返回的,看起来稍微有些不太好理解。

从代码组织的角度来讲一个函数只返回一个值(promise对象)是一个很是好的习惯,可是因为在外面不能访问 cancelableXHR 方法中建立的 req 变量,因此咱们须要编写一个专门的函数(上面的例子中的abort)来对这些内部对象进行处理。

固然也能够考虑到对返回的promise对象进行扩展,使其支持abort方法,可是因为promise对象是对值进行抽象化的对象,若是不加限制的增长操做用的方法的话,会使总体变得很是复杂。

你们都知道一个函数作太多的工做都不认为是一个好的习惯,所以咱们不会让一个函数完成全部功能,也许像下面这样对函数进行分割是一个不错的选择。

  • 返回包含XHR的promise对象

  • 接收promise对象做为参数并取消该对象中的XHR请求

将这些处理整理为一个模块的话,之后扩展起来也方便,一个函数所作的工做也会比较精炼,代码也会更容易阅读和维护。

咱们有不少方法来建立一个模块(AMD,CommonJS,ES6 module etc..),在这里,咱们将会把前面的 cancelableXHR 整理为一个Node.js的模块使用。

cancelableXHR.js
"use strict";
var requestMap = {};
function createXHRPromise(URL) {
    var req = new XMLHttpRequest();
    var promise = new Promise(function (resolve, reject) {
        req.open('GET', URL, true);
        req.onreadystatechange = function () {
            if (req.readyState === XMLHttpRequest.DONE) {
                delete requestMap[URL];
            }
        };
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.onabort = function () {
            reject(new Error('abort this req'));
        };
        req.send();
    });
    requestMap[URL] = {
        promise: promise,
        request: req
    };
    return promise;
}

function abortPromise(promise) {
    if (typeof promise === "undefined") {
        return;
    }
    var request;
    Object.keys(requestMap).some(function (URL) {
        if (requestMap[URL].promise === promise) {
            request = requestMap[URL].request;
            return true;
        }
    });
    if (request != null && request.readyState !== XMLHttpRequest.UNSENT) {
        request.abort();
    }
}
module.exports.createXHRPromise = createXHRPromise;
module.exports.abortPromise = abortPromise;

使用方法也很是简单,咱们经过 createXHRPromise 方法获得XHR的promise对象,当想对这个XHR进行abort操做的时候,将这个promise对象传递给 abortPromise(promise) 方法就能够了。

var cancelableXHR = require("./cancelableXHR");

var xhrPromise = cancelableXHR.createXHRPromise('http://httpbin.org/get');
xhrPromise.catch(function (error) {
    // 调用 abort 抛出的错误
});
cancelableXHR.abortPromise(xhrPromise);
建立包装了XHR的promise对象
取消在1中建立的promise对象的请求操做

4.5.6. 总结

在这里咱们学到了以下内容。

  • 通过必定时间后变为解决状态的delayPromise

  • 基于delayPromise和Promise.race的超时实现方式

  • 取消XHR promise请求

  • 经过模块化实现promise对象和操做的分离

Promise能很是灵活的进行处理流程的控制,为了充分发挥它的能力,咱们须要注意不要将一个函数写的过于庞大冗长,而是应该将其分割成更小更简单的处理,并对以前JavaScript中提到的机制进行更深刻的了解。

4.6. 什么是 Promise.prototype.done ?

若是你使用过其余的Promise实现类库的话,可能见过用done代替then的例子。

这些类库都提供了 Promise.prototype.done 方法,使用起来也和 then 同样,可是这个方法并不会返回promise对象。

虽然 ES6 PromisesPromises/A+等在设计上并无对Promise.prototype.done 作出任何规定,可是不少实现类库都提供了该方法的实现。

在本小节中,咱们将会学习什么是 Promise.prototype.done ,以及为何不少类库都提供了对该方法的支持。

4.6.1. 使用done的代码示例

看一下实际使用done的代码的例子的话,应该就很是容易理解 done 方法的行为了。

promise-done-example.js
if (typeof Promise.prototype.done === 'undefined') {
    Promise.prototype.done = function (onFulfilled, onRejected) {
        this.then(onFulfilled, onRejected).catch(function (error) {
            setTimeout(function () {
                throw error;
            }, 0);
        });
    };
}
var promise = Promise.resolve();
promise.done(function () {
    JSON.parse('this is not json');    // => SyntaxError: JSON.parse
});
// => 请打开浏览器的开发者工具中的控制台窗口看一下

在前面咱们已经说过,promise设计规格并无对 Promise.prototype.done作出任何规定,所以在使用的时候,你能够使用已有类库提供的实现,也能够本身去实现。

咱们会在后面讲述如何去本身实现,首先咱们这里先对使用 then 和使用 done这两种方式进行一下比较。

使用then的场景
var promise = Promise.resolve();
promise.then(function () {
    JSON.parse("this is not json");
}).catch(function (error) {
    console.error(error);// => "SyntaxError: JSON.parse"
});

从上面咱们能够看出,二者之间有如下不一样点。

  • done 并不返回promise对象

    • 也就是说,在done以后不能使用 catch 等方法组成方法链

  • done 中发生的异常会被直接抛给外面

    • 也就是说,不会进行Promise的错误处理(Error Handling)

因为done 不会返回promise对象,因此咱们不难理解它只能出如今一个方法链的最后。

此外,咱们已经介绍过了Promise具备强大的错误处理机制,而done则会在函数中跳过错误处理,直接抛出异常。

为何不少类库都提供了这个和Promise功能相矛盾的函数呢?看一下下面Promise处理失败的例子,也许咱们多少就能理解其中缘由了吧。

4.6.2. 消失的错误

Promise虽然具有了强大的错误处理机制,可是(调试工具不能顺利运行的时候)这个功能会致使人为错误(human error)更加复杂,这也是它的一个缺点。

也许你还记得,咱们在 then or catch? 中也看到了相似的内容。

像下面那样,咱们看一个能返回promise对象的函数。

json-promise.js
function JSONPromise(value) {
    return new Promise(function (resolve) {
        resolve(JSON.parse(value));
    });
}

这个函数将接收到的参数传递给 JSON.parse ,并返回一个基于JSON.parse的promise对象。

咱们能够像下面那样使用这个Promise函数,因为 JSON.parse 会解析失败并抛出一个异常,该异常会被 catch 捕获。

function JSONPromise(value) {
    return new Promise(function (resolve) {
        resolve(JSON.parse(value));
    });
}
// 运行示例
var string = "非合法json编码字符串";
JSONPromise(string).then(function (object) {
    console.log(object);
}).catch(function(error){
    // => JSON.parse抛出异常时
    console.error(error);
});

若是这个解析失败的异常被正常捕获的话则没什么问题,可是若是编码时忘记了处理该异常,一旦出现异常,那么查找异常发生的源头将会变得很是棘手,这就是使用promise须要注意的一面。

忘记了使用catch进行异常处理的的例子
var string = "非合法json编码字符串";
JSONPromise(string).then(function (object) {
    console.log(object);
}); 
虽然抛出了异常,可是没有对该异常进行处理

若是是JSON.parse 这样比较好找的例子还算好说,若是是拼写错误的话,那么发生了Syntax Error错误的话将会很是麻烦。

typo错误
var string = "{}";
JSONPromise(string).then(function (object) {
    conosle.log(object);
});
存在conosle这个拼写错误

这这个例子里,咱们错把 console 拼成了 conosle ,所以会发生以下错误。

ReferenceError: conosle is not defined

可是,因为Promise的try-catch机制,这个问题可能会被内部消化掉。 若是在调用的时候每次都无遗漏的进行 catch 处理的话固然最好了,可是若是在实现的过程当中出现了这个例子中的错误的话,那么进行错误排除的工做也会变得困难。

这种错误被内部消化的问题也被称为 unhandled rejection ,从字面上看就是在Rejected时没有找到相应处理的意思。

这种unhandled rejection错误到底有多难检查,也依赖于Promise的实现。 好比 ypromise 在检测到 unhandled rejection 错误的时候,会在控制台上提示相应的信息。

Promise rejected but no error handlers were registered to it

另外, Bluebird 在比较明显的人为错误,即ReferenceError等错误的时候,会直接显示到控制台上。

"Possibly unhandled ReferenceError. conosle is not defined

原生(Native)的 Promise实现为了应对一样问题,提供了GC-based unhandled rejection tracking功能。

该功能是在promise对象被垃圾回收器回收的时候,若是是unhandled rejection的话,则进行错误显示的一种机制。

Firefox 或 Chrome 的原生Promise都进行了部分实现。

4.6.3. done的实现

做为方法论,在Promise中 done 是怎么解决上面提到的错误被忽略呢? 其实它的方法很简单直接,那就是必需要进行错误处理。

因为能够在 Promise上实现 done 方法,所以咱们看看如何对 Promise.prototype.done 这个Promise的prototype进行扩展。

promise-prototype-done.js
"use strict";
if (typeof Promise.prototype.done === "undefined") {
    Promise.prototype.done = function (onFulfilled, onRejected) {
        this.then(onFulfilled, onRejected).catch(function (error) {
            setTimeout(function () {
                throw error;
            }, 0);
        });
    };
}

那么它是如何将异常抛到Promise的外面的呢?其实这里咱们利用的是在setTimeout中使用throw方法,直接将异常抛给了外部。

setTimeout的回调函数中抛出异常
try{
    setTimeout(function callback() {
        throw new Error("error");
    }, 0);
}catch(error){
    console.error(error);
}
这个例外不会被捕获

关于为何异步的callback中抛出的异常不会被捕获的缘由,能够参考下面内容。

仔细看一下 Promise.prototype.done的代码,咱们会发现这个函数什么也没 return 。 也就是说, done按照「Promise chain在这里将会中断,若是出现了异常,直接抛到promise外面便可」的原则进行了处理。

若是实现和运行环境实现的比较完美的话,就能够进行 unhandled rejection 检测,done也不必定是必须的了。 另外像本小节中的 Promise.prototype.done同样,done也能够在既有的Promise之上进行实现,也能够说它没有进入到 ES6 Promises的设计规范之中。

本文中的 Promise.prototype.done 的实现方法参考了 promisejs.org 。

4.6.4. 总结

在本小节中,咱们学习了 Q 、 Bluebird 和 prfun 等Promise类库提供的 done 的基础和实现细节,以及done方法和 then 方法有什么区别等内容。

咱们也学到了 done 有如下两个特色。

  • done 中出现的错误会被做为异常抛出

  • 终结 Promise chain

和 then or catch? 中说到的同样,由Promise内部消化掉的错误,随着调试工具或者类库的改进,大多数状况下也许已经不是特别大的问题了。

此外,因为 done 不会有返回值,所以不能在它以后进行方法链的建立,为了实现Promise方法风格上的统一,咱们也能够使用done方法。

ES6 Promises 自己提供的功能并非特别多。 所以,我想不少时候可能须要咱们本身进行扩展或者使用第三方类库。

咱们好不容易将异步处理统一采用Promise进行统一处理,可是若是作过头了,也会将系统变得特别复杂,所以,保持风格的统一是Promise做为抽象对象很是重要的部分。

在 Promises: The Extension Problem (part 4) | getiblog 中,介绍了一些如何编写Promise扩展程序的方法。

  • 扩展 Promise.prototype 的方法

  • 利用 Wrapper/Delegate 建立抽象层

此外,关于 Delegate 的详细使用方法,也能够参考 Chapter 28. Subclassing Built-ins ,那里有详细的说明。

4.7. Promise和方法链(method chain)

在Promise中你能够将 then 和 catch 等方法连在一块儿写。这很是像DOM或者jQuery中的方法链。

通常的方法链都经过返回 this 将多个方法串联起来。

关于如何建立方法链,能够从参考 方法链的建立方法 - 余味(日语博客) 等资料。

另外一方面,因为Promise 每次都会返回一个新的promise对象 ,因此从表面上看和通常的方法链几乎如出一辙。

在本小节里,咱们会在不改变已有采用方法链编写的代码的外部接口的前提下,学习如何在内部使用Promise进行重写。

4.7.1. fs中的方法链

咱们下面将会以 Node.js中的fs 为例进行说明。

此外,这里的例子咱们更重视代码的易理解性,所以从实际上来讲这个例子可能并不算太实用。

fs-method-chain.js
"use strict";
var fs = require("fs");
function File() {
    this.lastValue = null;
}
// Static method for File.prototype.read
File.read = function FileRead(filePath) {
    var file = new File();
    return file.read(filePath);
};
File.prototype.read = function (filePath) {
    this.lastValue = fs.readFileSync(filePath, "utf-8");
    return this;
};
File.prototype.transform = function (fn) {
    this.lastValue = fn.call(this, this.lastValue);
    return this;
};
File.prototype.write = function (filePath) {
    this.lastValue = fs.writeFileSync(filePath, this.lastValue);
    return this;
};
module.exports = File;

这个模块能够将相似下面的 read → transform → write 这一系列处理,经过组成一个方法链来实现。

var File = require("./fs-method-chain");
var inputFilePath = "input.txt",
    outputFilePath = "output.txt";
File.read(inputFilePath)
    .transform(function (content) {
        return ">>" + content;
    })
    .write(outputFilePath);

transform 接收一个方法做为参数,该方法对其输入参数进行处理。在这个例子里,咱们对经过read读取的数据在前面加上了 >> 字符串。

4.7.2. 基于Promise的fs方法链

下面咱们就在不改变刚才的方法链对外接口的前提下,采用Promise对内部实现进行重写。

fs-promise-chain.js
"use strict";
var fs = require("fs");
function File() {
    this.promise = Promise.resolve();
}
// Static method for File.prototype.read
File.read = function (filePath) {
    var file = new File();
    return file.read(filePath);
};

File.prototype.then = function (onFulfilled, onRejected) {
    this.promise = this.promise.then(onFulfilled, onRejected);
    return this;
};
File.prototype["catch"] = function (onRejected) {
    this.promise = this.promise.catch(onRejected);
    return this;
};
File.prototype.read = function (filePath) {
    return this.then(function () {
        return fs.readFileSync(filePath, "utf-8");
    });
};
File.prototype.transform = function (fn) {
    return this.then(fn);
};
File.prototype.write = function (filePath) {
    return this.then(function (data) {
        return fs.writeFileSync(filePath, data)
    });
};
module.exports = File;

新增长的then 和catch均可以看作是指向内部保存的promise对象的别名,而其它部分从对外接口的角度来讲都没有改变,使用方法也和原来同样。

所以,在使用这个模块的时候咱们只须要修改 require 的模块名便可。

var File = require("./fs-promise-chain");
var inputFilePath = "input.txt",
    outputFilePath = "output.txt";
File.read(inputFilePath)
    .transform(function (content) {
        return ">>" + content;
    })
    .write(outputFilePath);

File.prototype.then 方法会调用 this.promise.then 方法,并将返回的promise对象赋值给了 this.promise 变量这个内部promise对象。

这究竟有什么奥妙么?经过如下的伪代码,咱们能够更容易理解这背后发生的事情。

var File = require("./fs-promise-chain");
File.read(inputFilePath)
    .transform(function (content) {
        return ">>" + content;
    })
    .write(outputFilePath);
// => 处理流程相似如下的伪代码
promise.then(function read(){
        return fs.readFileSync(filePath, "utf-8");
    }).then(function transform(content) {
         return ">>" + content;
    }).then(function write(){
        return fs.writeFileSync(filePath, data);
    });

看到 promise = promise.then(...) 这种写法,会让人觉得promise的值会被覆盖,也许你会想是否是promise的chain被截断了。

你能够想象为相似 promise = addPromiseChain(promise, fn); 这样的感受,咱们为promise对象增长了新的处理,并返回了这个对象,所以即便本身不实现顺序处理的话也不会带来什么问题。

4.7.3. 二者的区别

同步和异步

要说fs-method-chain.jsPromise版二者之间的差异,最大的不一样那就要算是同步和异步了。

若是在相似 fs-method-chain.js 的方法链中加入队列等处理的话,就能够实现几乎和异步方法链一样的功能,可是实现将会变得很是复杂,因此咱们选择了简单的同步方法链。

Promise版的话如同在 专栏: Promise只能进行异步处理?里介绍过的同样,只会进行异步操做,所以使用了promise的方法链也是异步的。

错误处理

虽然fs-method-chain.js里面并不包含错误处理的逻辑, 可是因为是同步操做,所以能够将整段代码用 try-catch 包起来。

在 Promise版 提供了指向内部promise对象的then 和 catch 别名,因此咱们能够像其它promise对象同样使用catch来进行错误处理。

fs-promise-chain中的错误处理
var File = require("./fs-promise-chain");
File.read(inputFilePath)
    .transform(function (content) {
        return ">>" + content;
    })
    .write(outputFilePath)
    .catch(function(error){
        console.error(error);
    });

若是你想在fs-method-chain.js中本身实现异步处理的话,错误处理可能会成为比较大的问题;能够说在进行异步处理的时候,仍是使用Promise实现起来比较简单。

4.7.4. Promise以外的异步处理

若是你很熟悉Node.js的話,那么看到方法链的话,你是否是会想起来 Stream 呢。

若是使用 Stream 的话,就能够免去了保存 this.lastValue 的麻烦,还能改善处理大文件时候的性能。 另外,使用Stream的话可能会比使用Promise在处理速度上会快些。

使用Stream进行read→transform→write
readableStream.pipe(transformStream).pipe(writableStream);

所以,在异步处理的时候并非说Promise永远都是最好的选择,要根据本身的目的和实际状况选择合适的实现方式。

Node.js的Stream是一种基于Event的技术

关于Node.js中Stream的详细信息能够参考如下网页。

4.7.5. Promise wrapper

再回到 fs-method-chain.js 和 Promise版,这两种方法相比较内部实现也很是相近,让人以为是否是同步版本的代码能够直接就当作异步方式来使用呢?

因为JavaScript能够向对象动态添加方法,因此从理论上来讲应该能够从非Promise版自动生成Promise版的代码。(固然静态定义的实现方式容易处理)

尽管 ES6 Promises 并无提供此功能,可是著名的第三方Promise实现类库 bluebird 等提供了被称为 Promisification 的功能。

若是使用相似这样的类库,那么就能够动态给对象增长promise版的方法。

var fs = Promise.promisifyAll(require("fs"));

fs.readFileAsync("myfile.js", "utf8").then(function(contents){
    console.log(contents);
}).catch(function(e){
    console.error(e.stack);
});
Array的Promise wrapper

前面的 Promisification 具体都干了些什么光凭想象恐怕不太容易理解,咱们能够经过给原生的 Array 增长Promise版的方法为例来进行说明。

在JavaScript中原生DOM或String等也提供了不少建立方法链的功能。 Array 中就有诸如 map 和 filter 等方法,这些方法会返回一个数组类型,能够用这些方法方便的组建方法链。

array-promise-chain.js
"use strict";
function ArrayAsPromise(array) {
    this.array = array;
    this.promise = Promise.resolve();
}
ArrayAsPromise.prototype.then = function (onFulfilled, onRejected) {
    this.promise = this.promise.then(onFulfilled, onRejected);
    return this;
};
ArrayAsPromise.prototype["catch"] = function (onRejected) {
    this.promise = this.promise.catch(onRejected);
    return this;
};
Object.getOwnPropertyNames(Array.prototype).forEach(function (methodName) {
    // Don't overwrite
    if (typeof ArrayAsPromise[methodName] !== "undefined") {
        return;
    }
    var arrayMethod = Array.prototype[methodName];
    if (typeof  arrayMethod !== "function") {
        return;
    }
    ArrayAsPromise.prototype[methodName] = function () {
        var that = this;
        var args = arguments;
        this.promise = this.promise.then(function () {
            that.array = Array.prototype[methodName].apply(that.array, args);
            return that.array;
        });
        return this;
    };
});

module.exports = ArrayAsPromise;
module.exports.array = function newArrayAsPromise(array) {
    return new ArrayAsPromise(array);
};

原生的 Array 和 ArrayAsPromise 在使用时有什么差别呢?咱们能够经过对 上面的代码 进行测试来了解它们之间的不一样点。

array-promise-chain-test.js
"use strict";
var assert = require("power-assert");
var ArrayAsPromise = require("../src/promise-chain/array-promise-chain");
describe("array-promise-chain", function () {
    function isEven(value) {
        return value % 2 === 0;
    }

    function double(value) {
        return value * 2;
    }

    beforeEach(function () {
        this.array = [1, 2, 3, 4, 5];
    });
    describe("Native array", function () {
        it("can method chain", function () {
            var result = this.array.filter(isEven).map(double);
            assert.deepEqual(result, [4, 8]);
        });
    });
    describe("ArrayAsPromise", function () {
        it("can promise chain", function (done) {
            var array = new ArrayAsPromise(this.array);
            array.filter(isEven).map(double).then(function (value) {
                assert.deepEqual(value, [4, 8]);
            }).then(done, done);
        });
    });
});

咱们看到,在 ArrayAsPromise 中也能使用 Array的方法。并且也和前面的例子相似,原生的Array是同步处理,而 ArrayAsPromise则是异步处理,这也是它们的不一样之处。

仔细看一下 ArrayAsPromise 的实现,也许你已经注意到了, Array.prototype 的全部方法都被实现了。 可是,Array.prototype 中也存在着相似array.indexOf 等并不会返回数组类型数据的方法,这些方法若是也要支持方法链的话就有些不天然了。

在这里很是重要的一点是,咱们能够经过这种方式,为具备接收相同类型数据接口的API动态的建立Promise版的API。 若是咱们能意识到这种API的规则性的话,那么就可能发现一些新的使用方法。

前面咱们看到的 Promisification 方法,借鉴了了 Node.js的Core模块中在进行异步处理时将 function(error,result){} 方法的第一个参数设为 error 这一规则,自动的建立由Promise包装好的方法。

4.7.6. 总结

在本小节咱们主要学习了下面的这些内容。

  • Promise版的方法链实现

  • Promise并非老是异步编程的最佳选择

  • Promisification

  • 统一接口的重用

ES6 Promises只提供了一些Core级别的功能。 所以,咱们也许须要对现有的方法用Promise方式从新包装一下。

可是,相似Event等调用次数没有限制的回调函数等在并不适合使用Promise,Promise也不能说何时都是最好的选择。

至于什么状况下应该使用Promise,何时不应使用Promise,并非本书要讨论的目的, 咱们须要牢记的是不要什么都用Promise去实现,我想最好根据本身的具体目的和状况,来考虑是应该使用Promise仍是其它方法。

4.8. 使用Promise进行顺序(sequence)处理

在第2章 Promise.all 中,咱们已经学习了如何让多个promise对象同时开始执行的方法。

可是 Promise.all 方法会同时运行多个promise对象,若是想进行在A处理完成以后再开始B的处理,对于这种顺序执行的话 Promise.all就无能为力了。

此外,在同一章的Promise和数组 中,咱们也介绍了一种效率不是特别高的,使用了 重复使用多个then的方法 来实现如何按顺序进行处理。

在本小节中,咱们将对如何在Promise中进行顺序处理进行介绍。

4.8.1. 循环和顺序处理

在 重复使用多个then的方法 中的实现方法以下。

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };
function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    // [] 用来保存初始化的值
    var pushValue = recordValue.bind(null, []);
    return request.comment().then(pushValue).then(request.people).then(pushValue);
}
// 运行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

使用这种写法的话那么随着 request 中元素数量的增长,咱们也须要不断增长对 then 方法的调用

所以,若是咱们将处理内容统一放到数组里,再配合for循环进行处理的话,那么处理内容的增长将不会再带来什么问题。首先咱们就使用for循环来完成和前面一样的处理。

promise-foreach-xhr.js
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };
function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    // [] 用来保存初始化值
    var pushValue = recordValue.bind(null, []);
    // 返回promise对象的函数的数组
    var tasks = [request.comment, request.people];
    var promise = Promise.resolve();
    // 开始的地方
    for (var i = 0; i < tasks.length; i++) {
        var task = tasks[i];
        promise = promise.then(task).then(pushValue);
    }
    return promise;
}
// 运行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

使用for循环的时候,如同咱们在 专栏: 每次调用then都会返回一个新建立的promise对象 以及 Promise和方法链 中学到的那样,每次调用 Promise#then 方法都会返回一个新的promise对象。

所以相似 promise = promise.then(task).then(pushValue); 的代码就是经过不断对promise进行处理,不断的覆盖 promise 变量的值,以达到对promise对象的累积处理效果。

可是这种方法须要 promise 这个临时变量,从代码质量上来讲显得不那么简洁。

若是将这种循环写法改用 Array.prototype.reduce 的话,那么代码就会变得聪明多了。

4.8.2. Promise chain和reduce

若是将上面的代码用 Array.prototype.reduce 重写的话,会像下面同样。

promise-reduce-xhr.js
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };
function main() {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    var tasks = [request.comment, request.people];
    return tasks.reduce(function (promise, task) {
        return promise.then(task).then(pushValue);
    }, Promise.resolve());
}
// 运行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

这段代码中除了 main 函数以外的其余处理都和使用for循环的时候相同。

Array.prototype.reduce 的第二个参数用来设置盛放计算结果的初始值。在这个例子中, Promise.resolve() 会赋值给 promise,此时的 task 为 request.comment 。

在reduce中第一个参数中被 return 的值,则会被赋值为下次循环时的 promise 。也就是说,经过返回由 then 建立的新的promise对象,就实现了和for循环相似的 Promise chain 了。

下面是关于 Array.prototype.reduce 的详细说明。

使用reduce和for循环不一样的地方是reduce再也不须要临时变量 promise 了,所以也不用编写 promise = promise.then(task).then(pushValue); 这样冗长的代码了,这是很是大的进步。

虽然 Array.prototype.reduce 很是适合用来在Promise中进行顺序处理,可是上面的代码有可能让人难以理解它是如何工做的。

所以咱们再来编写一个名为 sequenceTasks 的函数,它接收一个数组做为参数,数组里面存放的是要进行的处理Task。

从下面的调用代码中咱们能够很是容易的从其函数名想到,该函数的功能是对 tasks 中的处理进行顺序执行了。

var tasks = [request.comment, request.people];
sequenceTasks(tasks);

4.8.3. 定义进行顺序处理的函数

基本上咱们只须要基于 使用reduce的方法 重构出一个函数。

promise-sequence.js
function sequenceTasks(tasks) {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    return tasks.reduce(function (promise, task) {
        return promise.then(task).then(pushValue);
    }, Promise.resolve());
}

须要注意的一点是,和 Promise.all 等不一样,这个函数接收的参数是一个函数的数组。

为何传给这个函数的不是一个promise对象的数组呢?这是由于promise对象建立的时候,XHR已经开始执行了,所以再对这些promise对象进行顺序处理的话就不能正常工做了。

所以 sequenceTasks 将函数(该函数返回一个promise对象)的数组做为参数。

最后,使用 sequenceTasks 重写最开始的例子的话,以下所示。

promise-sequence-xhr.js
function sequenceTasks(tasks) {
    function recordValue(results, value) {
        results.push(value);
        return results;
    }
    var pushValue = recordValue.bind(null, []);
    return tasks.reduce(function (promise, task) {
        return promise.then(task).then(pushValue);
    }, Promise.resolve());
}
function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
var request = {
        comment: function getComment() {
            return getURL('http://azu.github.io/promises-book/json/comment.json').then(JSON.parse);
        },
        people: function getPeople() {
            return getURL('http://azu.github.io/promises-book/json/people.json').then(JSON.parse);
        }
    };
function main() {
    return sequenceTasks([request.comment, request.people]);
}
// 运行示例
main().then(function (value) {
    console.log(value);
}).catch(function(error){
    console.error(error);
});

怎样, main() 中的流程是否是更清晰易懂了。

如上所述,在Promise中,咱们能够选择多种方法来实现处理的按顺序执行。

可是,这些方法都是基于JavaScript中对数组及进行操做的for循环或 forEach 等,本质上并没有大区别。 所以从必定程度上来讲,在处理Promise的时候,将大块的处理分红小函数来实现是一个很是好的实践。

4.8.4. 总结

在本小节中,咱们对如何在Promise中进行和 Promise.all 相反,按顺序让promise一个个进行处理的实现方式进行了介绍。

为了实现顺序处理,咱们也对从过程风格的编码方式到自定义顺序处理函数的方式等实现方式进行了介绍,也再次强调了在Promise领域咱们应遵循将处理按照函数进行划分的基本原则。

在Promise中若是还使用了Promise chain将多个处理链接起来的话,那么还可能使源代码中的一条语句变得很长。

这时候若是咱们回想一下这些编程的基本原则进行函数拆分的话,代码总体结构会变得很是清晰。

此外,Promise的构造函数以及 then 都是高阶函数,若是将处理分割为函数的话,还能获得对函数进行灵活组合使用的反作用,意识到这一点对咱们也会有一些帮助的。

高阶函数指的是一个函数能够接受其参数为函数对象的实例

5. Promises API Reference

5.1. Promise#then

promise.then(onFulfilled, onRejected);
then代码示例
var promise = new Promise(function(resolve, reject){
    resolve("传递给then的值");
});
promise.then(function (value) {
    console.log(value);
}, function (error) {
    console.error(error);
});

这段代码建立一个promise对象,定义了处理onFulfilled和onRejected的函数(handler),而后返回这个promise对象。

这个promise对象会在变为resolve或者reject的时候分别调用相应注册的回调函数。

  • 当handler返回一个正常值的时候,这个值会传递给promise对象的onFulfilled方法。

  • 定义的handler中产生异常的时候,这个值则会传递给promise对象的onRejected方法。

5.2. Promise#catch

promise.catch(onRejected);
catch代码示例
var promise = new Promise(function(resolve, reject){
    resolve("传递给then的值");
});
promise.then(function (value) {
    console.log(value);
}).catch(function (error) {
    console.error(error);
});

这是一个等价于promise.then(undefined, onRejected) 的语法糖。

5.3. Promise.resolve

Promise.resolve(promise);
Promise.resolve(thenable);
Promise.resolve(object);
Promise.resolve代码示例
var taskName = "task 1"
asyncTask(taskName).then(function (value) {
    console.log(value);
}).catch(function (error) {
    console.error(error);
});
function asyncTask(name){
    return Promise.resolve(name).then(function(value){
        return "Done! "+ value;
    });
}

根据接收到的参数不一样,返回不一样的promise对象。

虽然每种状况都会返回promise对象,可是大致来讲主要分为下面3类。

接收到promise对象参数的时候

返回的仍是接收到的promise对象

接收到thenable类型的对象的时候

返回一个新的promise对象,这个对象具备一个 then 方法

接收的参数为其余类型的时候(包括JavaScript对或null等)

返回一个将该对象做为值的新promise对象

5.4. Promise.reject

Promise.reject(object)
Promise.reject代码示例
var failureStub = sinon.stub(xhr, "request").returns(Promise.reject(new Error("bad!")));

返回一个使用接收到的值进行了reject的新的promise对象。

而传给Promise.reject的值也应该是一个 Error 类型的对象。

另外,和 Promise.resolve不一样的是,即便Promise.reject接收到的参数是一个promise对象,该函数也仍是会返回一个全新的promise对象。

var r = Promise.reject(new Error("error"));
console.log(r === Promise.reject(r));// false

5.5. Promise.all

Promise.all(promiseArray);
Promise.all代码示例
var p1 = Promise.resolve(1),
    p2 = Promise.resolve(2),
    p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(function (results) {
    console.log(results);  // [1, 2, 3]
});

生成并返回一个新的promise对象。

参数传递promise数组中全部的promise对象都变为resolve的时候,该方法才会返回, 新建立的promise则会使用这些promise的值。

若是参数中的任何一个promise为reject的话,则整个Promise.all调用会当即终止,并返回一个reject的新的promise对象。

因为参数数组中的每一个元素都是由 Promise.resolve 包装(wrap)的,因此Paomise.all能够处理不一样类型的promose对象。

5.6. Promise.race

Promise.race(promiseArray);
Promise.race代码示例
var p1 = Promise.resolve(1),
    p2 = Promise.resolve(2),
    p3 = Promise.resolve(3);
Promise.race([p1, p2, p3]).then(function (value) {
    console.log(value);  // 1
});

生成并返回一个新的promise对象。

参数 promise 数组中的任何一个promise对象若是变为resolve或者reject的话, 该函数就会返回,并使用这个promise对象的值进行resolve或者reject。

6. 用語集

Promises

Promise规范自身

promise对象

promise对象指的是 Promise 实例对象

ES6 Promises

若是想明确表示使用 ECMAScript 6th Edition 的话,能够使用ES6做为前缀(prefix)

Promises/A+

Promises/A+。 这是ES6 Promises的前身,是一个社区规范,它和 ES6 Promises 有不少共通的内容。

Thenable

类Promise对象。 拥有名为.then方法的对象。

promise chain

指使用 then 或者 catch 方法将promise对象链接起来的行为。 此用语只是在本书中的说法,而不是在 ES6 Promises 中定义的官方用语。

w3ctag/promises-guide

Promises指南 - 这里有不少关于概念方面的说明

domenic/promises-unwrapping

ES6 Promises规范的repo - 能够经过查看issue来了解各类关于规范的前因后果和信息

ECMAScript Language Specification ECMA-262 6th Edition – DRAFT

ES6 Promises的规范 - 若是想参考关于ES6 Promises的规范,则应该先看这里

JavaScript Promises: There and back again - HTML5 Rocks

关于Promises的文章 - 这里的示例代码和参考(reference)的完成度都很高

Node.js Promise再次降临! - ぼちぼち日記

关于Node.js和Promise的文章 - thenable部分参考了本文

8. 关于做者

azu azu (Twitter : @azu_re )

关注浏览器、JavaScript相关的最新技术。

擅长将目的做为手段,本书也是所以而成。

管理着我的主页 Web Scratch 和 JSer.info 。

9. 关于译者

9.1. 给原著者留言、后记

后记.pdf 里面记录了笔者为何要写这么一本书,编写的过程,以及如何进行测试。

你能够在Gumroad以避免费的价格或者本身设定一个任意的价格来下载本书的后记。

在下载的时候,会有一个给做者留言的地方, 但愿各位读者能写下一点什么以后下载。

若是本书有任何问题的话,也能够经过 GitHub或者Gitter 来提交。