一块儿来学Promise

注意,本文主要针对ES6标准实现的Promise语法进行阐述,实例代码也都使用ES6语法,快速入门ES6请参见ECMAScript 6 扫盲javascript

一分钟快速入门

被回调地狱整怕了?快试Promise吧!。Promise的核心思想其实很简单,就是将异步操做结果处理交给Promise对象的方法注册,而后等到异步操做完了再去取用这些处理操做。至于取用哪一个处理操做,就得看Promise对象状态了。Promise对象一共有三种状态:Pending(初始状态)、Fulfilled(异步操做成功)、Rejected(异步操做失败)。而三者间的转换只有两种状况:Pending—>Fulfilled、Pending—>Rejected;详见下图:html

prmoise-sates

了解了状态及其转换后,咱们就能够来使用Promise对象了:前端

let promise = new Promise((resolve, reject)=> {
    // 异步操做
    // 异步操做成功时调用
    resolve(value)
    // 异步操做失败时调用
    reject(error)
    });

上述代码中传给Promise构造函数的两个函数resolve, reject,分别用于触发Promise对象的Fullfilled和Rejected状态。当处于Fullfilled状态时Promise会调用then方法,而处于Rejected状态时则会调用catch方法,这两个方法都会返回Promise对象,因此咱们能够采用链式写法:java

promise.then((value)=> {...})
    .catch((error)=> {...});

上面的方法链中,then方法里注册了Fullfilled状态的处理函数、catch方法则注册了Rejected状态的处理函数。这种简单明了的写法把异步操做的结果处理函数分离了出来,若是这些处理自己又是异步操做,那咱们天然也就把层层异步回调也从回调地狱中剥离了,代码瞬间清爽有木有!node

深刻Promise调用链

前面咱们只是将一层处理操做分离到then方法中(其中catch方法只是then方法的一个语法糖,后面会再做讲解);但在实际应用中多个异步操做每每会以串行或并行的方式连续出现,好比下面这个预约房间的流程:es6

order-room

其中数据校验、向API发送请求、往数据库插入数据都是异步操做,一种用回调的写法大概长这样:ajax

validate(data, (err)=> {
    if (err) return errorHandler(err);
    request(apiUrl, (err, apiResponse)=> {
            if (err) return errorHandler(err);
            if (apiResponse.isSuccessful) insertToDB(data, (err)=> {
                    if (err) return errorHandler(err);
                    successHandler();
                });
            else errorHandler(new Error('API error'));
        });
    });

根据前面咱们了解的Promise用法,咱们已经能将validate这个异步操做写成Promise形式了:数据库

let promiseValidate = new Promise((resolve, reject)=> {
    validate(data, (err)=> {
        if (err) return reject(err);
        resolve();
        });
    });

promiseValidate(data)
    .then(()=> {
        request(apiUrl, (err, apiResponse)=> {
                if (err) return errorHandler(err);
                if (apiResponse.isSuccessful) insertToDB(data, (err)=> {
                        if (err) return errorHandler(err);
                        successHandler();
                    });
                else errorHandler(new Error('API error'));
            });
        })
    .catch((err)=> errorHandler(err));

但要改就改到底,上面这种Promise和回调写法混合得就不三不四,除了仍存在回调嵌套的问题,屡次出现的错误判断和处理也有点违反DRY。因此接下来咱们会深刻研究下Promise调用链的行为,重点探讨then方法里注册的回调对调用链上数据传递和Promise对象状态变化的影响,以及如何在调用链上对错误进行统一的处理。segmentfault

Promise.resolve和Promise.reject

咱们先来看下一种“快速”生成Promise对象的方法:直接调用Promise.resolve(value)Promise.reject(err)。这种方法和new一个Promise对象的区别在于,Promise对象在生成的时候状态就已经肯定,要么是Fullfilled(使用Promise.resolve())、要么是Rejected(使用Promise.reject()),不会和new实例化同样等要异步操做完了再发生变化。api

此外,若是传给Promise.resolve方法的是一个具备then方法的对象(即所谓的Thenable对象),好比jQuery的$.ajax(),那么返回的Promise对象,后续调用的then就是原对象then方法的同一形式(参见下面的代码)。简单来说,就是Promise.resolve会将Thenable对象转为ES6的Promise对象,这一特性常被用来将Promise的不一样实现转换为ES6实现。

$.ajax('https://httpbin.org/ip').then((value)=> {
    /* 输出223.65.191.59 */
    console.log(value.origin)
    });

Promise.resolve($.ajax('https://httpbin.org/ip'))
    .then((value)=> {
        /* 输出223.65.191.59 */
        console.log(value.origin)
        });

详解Promise.prototype.then

有了前面知识的铺垫,咱们终于能够来详细讲一下Promise对象的then方法了。

参数

如前面所提到的,catch方法只是then方法的一个语法糖,
缘由就在于then方法的参数为其实是“两个”回调函数,分别用于处理调用它的Promise对象的Fullfilled和Rejected状态,而catch方法就等价于then(undefined, Rejected状态处理函数)

关于这两个回调函数,首先要注意它们是异步调用的:

var v = 1;
/* 输出result: 2 */
Promise.resolve().then(()=> {console.log('result: ' + v)});
/* 输出result: 2 */
Promise.reject().then(undefined, ()=> {console.log('result: ' + v)});
v++;

而两个回调函数的参数,则是经过调用then方法的Promise对象指定的:

  • new Promise()产生的Promise对象,会分别用内部resolve()reject()函数的参数

  • Promise.resolve()Promise.reject()产生的Promise对象,则分别用Promise.resolve()Promise.reject()的参数

而两个回调函数的返回值,会用Promise.resolve(第一个回调返回值)Promise.reject(第二个回调返回值)的形式做包装,用来“替换”then方法返回的Promise对象。结合上面提到的then回调函数参数指定方式,回调返回值会这样影响下一个then的回调函数:

  • 返回的是普通数据,会传给下一级调用的then方法做为回调函数的参数

  • 返回的是Promise对象或Thenable对象,会被拿来“替换”then方法返回的Promise对象,具体then的回调函数怎么调用和传参就得看其内部实现了

返回值

一个新的Promise对象,状态看执行哪一个回调函数决定。注意这是一个新对象,不是简单把调用then的Promise对象拿来改装后返回:

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

链式调用

知道了then方法的具体细节后,咱们就能明白Promise调用链上:

  • 传递数据的方法:利用上面提到的then回调的参数传递形式——不管是在Promise对象产生过程当中直接传递、仍是在then回调返回值中间接传递——就能实现将每一级异步操做的结果传递给后续then中注册的处理函数处理。

  • Promise对象状态传递和改变的方法:利用then回调的返回值,能够控制某个操做后then方法返回的Promise对象及其状态。

如今咱们把全部异步操做改成Promise语法,再利用在Promise调用链传递数据和控制状态的方法,就能把本节开始提到的预约房间操做中的回调嵌套都展开来了:

let promiseValidate = new Promise((resolve, reject)=> {
    validate(data, (err)=> {
        if (err) return reject(err);
        resolve();
        });
    });

let promiseRequest = new Promise((resolve, reject)=> {
    request(data, (err, apiResponse)=> {
        if (err) return reject(err);
        // 在Promise对象产生过程当中直接传递异步操做的结果
        resolve(apiResponse);
        });
    }
);

let promiseInsertToDB = new Promise((resolve, reject)=> {
    insertToDB(data, (err)=> {
        if (err) return reject(err);
        resolve();
        });
    }
);

promiseValidate(data)
    .then(()=> promiseRequest(apiUrl))
    .then((apiResponse)=> {
        // 控制then回调的返回值,来改变then方法返回的新Promise对象的状态
        if (apiResponse.isSuccessful) return insertToDB(data);
        else errorHandler(new Error('API error'));
        })
    .then(()=> successHandler())
    .catch((err)=> return errorHandler(err));

上面的代码不只将嵌套的代码展开,让咱们挣脱了“回调地狱”;并且能够对异步操做的错误直接利用统一的Promise错误处理方法,避免写一堆重复的代码。若是要进一步DRY,能够抽象出一个将典型的Node.js回调接口封装为Promise接口的函数:

/* 处理形如 receiver.fn(...args, (err, res)=> {}) 的接口 */
let promisify = (fn, receiver) => {
  return (...args) => { // 返回从新封装的Promise接口
    return new Promise((resolve, reject) => {
      fn.apply(receiver, [...args, (err, res) => { // 从新绑定this
        return err ? reject(err) : resolve(res);
      }]);
    });
  };
};

/* 用例 */
let promiseValidate = promisify(validate, global);
let promiseRequest = promisify(request, global);
let promiseInsertToDB = promisify(insertToDB, global);

注意,因为resolve和reject方法只能接收一个参数,所上面这个函数处理的回调里只能有err和一个数据参数。

Promise调用链上的错误处理

在Promise调用链上的处理错误的思路,就是去触发Promise对象的Rejected状态,利用状态的传递特性实现对错误的捕获,再在catchthen回调里处理这些错误。下面咱们就来进行相关的探讨:

错误的捕获

首先咱们有必要详细了解下Promise对象的Rejected状态的产生和传递过程。

Rejected状态的产生有两种状况:

  • 调用了reject函数:Promise对象实例化的回调调用了reject(),或者直接调用了Promise.reject()

  • 经过throw抛出错误

而只要产生了Rejected状态,就会在调用链上持续传递,直到碰见Rejected状态的处理回调(catch的回调或then的第二个回调)。再结合以前提到的Promise调用链上的数据传递方法,错误就能在调用链上做为参数被相应的回调“捕获”了。这个过程能够参见下图:

promise-reject-flow

这里要注意,经过throw抛出错时,若是错误是在setTimeout等的回调中抛出,是不会让Promise对象产生Rejected状态的,这也觉得着Promise调用链上捕获不了这个错误。举个例子,下面这段代码就不会有任何输出:

Promise.resolve()
    .then(()=> setTimeout(100, ()=> {throw new Error('hi')}))
    .catch((err)=> console.log(err));

究其缘由,是由于setTimeout的异步操做和Promise的异步操做不属于同一种任务队列,setTimeout回调里的错误会直接抛到全局变成Uncaught Error,而不会做用到Promise对象及其调用链上。这就也意味着,想要保证在调用链上产生的错误能被捕获,就必须始终使用调用reject函数的方式来产生和传递错误。

错误处理

错误处理能够在catch的回调或then的第二个回调里进行。虽然前面提到catch方法等价于then(undefined, Rejected状态处理函数),但推荐始终使用catch来处理错误,缘由有两个:

  • 代码的可读性

  • 对于then(Fullfilled状处理函数, Rejected状态的处理函数)这种写法,若是Fullfilled状态的处理函数里出错了,那错误只会继续向下传递,同级的Rejected状态处理函数没办法捕获该错误

优化房间预订例子的错误处理

了解完了Promise调用链上的错误处理,咱们再来回顾一开始提到的房间预订例子。以前咱们的代码里只是对异步操做中的可能出现错误进行了统一的处理,可是其中的API error等别的执行错误并未使用在Promise调用链上捕获和处理错误的方式。为了进一步DRY,咱们能够经过调用Promise.reject,强制将返回的Promise对象变为Rejected状态,共用统一的Promise错误处理:

(apiResponse)=> {
        if (apiResponse.isSuccessful) return insertToDB(data);
        // 返回的Promise对象为Rejected状态,共用统一的Promise错误处理
        else return Promise.reject(new Error('API error'));
        }

Promise.all和Promise.race

前面研究的多个异步操做间每每具备先后依赖关系,或者说它们是“串行”进行的,只有前一个完成了才能进行后一个。但有时咱们处理的异步操做间可能并不具备依赖关系,好比处理多张图片,这时再使用上面的调用链写法,就只能等处理完一张图片、对应的Promise对象状态变化了,才能再去处理下一张,就显得很低效了。因此,咱们须要一种能在调用链中同时处理多个Promise对象的方法,Promise.allPromise.race就是这样应运而生的。

这两个方法的相同点是会接受一个Promise对象组成的数组做为参数,包装返回成一个新的Promise实例。而它们的区别就在于返回的这个Promise实例状态如何变化:

  • Promise.all

    • 全部传入的Promise对象状态都变成Fullfilled,最终状态才会变成Fullfilled;此时便会调用Promise.resolve(各Promise对象resolve参数组成的数组),生成新状态的Promise对象返回

    • 各个Promise对象如有一个被reject,最终状态就变成Rejected;此时便会调用Promise.reject(第一个被reject的实例的reject参数),生成新状态的Promise对象返回

  • Promise.race:只要传入的各个Promise对象中有一个率先改变状态(Fullfilled或Rejected),返回的Promise对象状态就会改变为相应状态

有了这两个方法,咱们就能在Promise调用链上“并行”等待某些异步操做了,仍是用前面提到的客房例子来举例,若是咱们在预约房间时须要请求的API不止一个,调用链能够这么写:

promiseValidate(data)
    /* 请求多个API */
    .then(()=> Promise.all([promiseRequest(apiUrl1), promiseRequest(apiUrl2)]))
    .then((apiResponse)=> {
        /* 传给下个then回调的是一个resolve参数组成的数组 */
        if (apiResponse[0].isSuccessful && apiResponse[1].isSuccessful) return insertToDB(data);
        else return Promise.reject(new Error('API error'));
        })
    .then(()=> successHandler())
    .catch((err)=> return errorHandler(err));

Promise的应用

Promise是一种异步调用的写法,天然是用来写出清晰的异步代码、让咱们摆脱回调写法带来的种种弊端,本文一直使用的预约房间例子就是一个佐证。不过考虑实际的应用场景,仍是有一些须要注意的地方:

前端异步处理

前端的浏览器兼容性是阻碍新技术运用的一大难题,虽然使目前浏览器对于ES6的支持愈来愈完善了,但除非你不考虑IE(兼容性表),不然在前端代码里直接使用的原生的Promise实现并不太现实。对于这种状况,咱们能够用一些Polyfill或拓展类库来让咱们能写Promise代码。

Node的异步处理:

Node.js环境下对ES6的Promise支持,在零点几版开始就有了,因此咱们在编写服务器代码、或者写一些跑在Node上的模块时能够直接上Promise语法。不过要注意的是,Node上的大部分模块开放的API,仍是默认使用回调风格,这是为了方便用户在不了解Promise语法时快速上手;因此通常本身写的模块API也会遵循这个惯例,至于模块内部实现那就随你的意愿使用了。

还有一个要值得注意的是,最近Node实现了更优雅的异步写法--async函数,不过新的写法是基于Promise实现的,因此虽然async函数的出现让Promise有种高不成低不就的感受,但了解Promise的用法仍是颇有必要的,但愿本文能帮你作到这点:D。

参考

JavaScript Promise迷你书
Promise 的链式调用与停止
如何把 Callback 接口包装成 Promise 接口

相关文章
相关标签/搜索