本文是基于对阮一峰的Promise文章的学习整理笔记,整理了文章的顺序、增长了更多的例子,使其更好理解。javascript
在Promise以前,在js中的异步编程都是采用回调函数和事件的方式,可是这种编程方式在处理复杂业务的状况下,很容易出现callback hell(回调地狱)
,使得代码很难被理解和维护。java
Promise就是改善这种情形的异步编程的解决方案,它由社区最先提出和实现,es6将其写进了语言标准,统一了用法,而且提供了一个原生的对象Promise
。node
咱们经过一个简单例子先来感觉一下Promise。git
var p = new Promise(function (resolve, reject) { // ... if(/* 异步操做成功 */){ resolve(ret); } else { reject(error); } }); p.then(function (value) { // 完成态 }, function (error) { // 失败态 });
咱们须要关注的是es6
Promise的构造函数github
resolve() , reject()chrome
then()编程
咱们在经过Promise构造函数实例化一个对象时,会传递一个函数做为参数,那么这个函数有什么特色?数组
答案就是在新建一个Promise后,这个函数会当即执行。promise
let promise = new Promise(function (reslove, reject) { console.log('Promise'); }); console.log('end');
执行结果以下:
能够看到是先输出了Promise
,再输出了end
。
在Promise中,对一个异步操做作出了抽象的定义,Promise操做只会处在3种状态的一种,他们之间的转化如图所示
注意,这种状态的改变只会出现从未完成态向完成态或失败态转化,不能逆反。完成态和失败态不能互相转化,并且,状态一旦转化,将不能更改。
只有异步操做的结果能够决定当前是哪种状态,任何其余操做都没法改变这个状态。这也是Promise这个名字的由来,它的英语意思是承诺,表示其余手段没法改变。
在声明一个Promise对象实例时,咱们传入的匿名函数参数中:
resolve
就对应着完成态以后的操做
reject
对应着失败态以后的操做
那么问题来了,then()方法有什么做用?resolve和reject又是从哪里传递过来的?
其实这两个问题是一个问题,在实例化一个Promise对象以后,咱们调用该对象实例的then()
方法传递的两个参数中:
第一个参数(函数)对应着完成态的操做,也就是resolve
第二个参数(函数)对应着失败态的操做,也就是reject
那就是说,在Promise中是经过then()方法来指定处理异步操做结果的方法。
到这里咱们明白了Promise的语法,也了解了Promise中函数是如何执行的,结合一个实际的案例,来加深对Promise的理解。
咱们来实现一个异步加载图片的函数
function loadImageAsync(url) { return new Promise(function (reslove, reject) { var img = new Image(); img.onload = function () { reslove(); } img.onerror = function () { reject(); } console.log("loading image"); img.src = url; }); } var loadImage1 = loadImageAsync("https://www.google.co.jp/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png"); loadImage1.then(function success() { console.log("success"); }, function fail() { console.log("fail"); }); var loadImage2 = loadImageAsync("1.png"); loadImage2.then(function success() { console.log("success"); }, function fail() { console.log("fail"); });
咱们在chrome中执行,先是传递一个有效的url,再传递一个无效的url,执行的效果为:
reject
函数的参数通常来讲是Error对象的实例,而resolve
函数的参数除了正常的值外,还多是另外一个Promise实例
,表示异步操做的结果有多是一个值,也有多是另外一个异步操做。
var p1 = new Promise( function(resolve, reject) { // ... }); var p2 = new Promise( function(resolve, reject) { // ... resolve(p1); });
代码分析:p1和p2都是Promise的实例,p2中的resolve方法将p1做为参数,即一个异步操做的结果是返回另外一个异步操做。
注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态,他们之间的关系是
举个例子
console.time('Promise example start') var p1 = new Promise( (resolve, reject) => { setTimeout(() => resolve('hi'), 3000); }); var p2 = new Promise( (resolve, reject) => { setTimeout(() => resolve(p1), 10); }); p2.then( ret => { console.log(ret); console.timeEnd('Promise example end') });
咱们在node环境下运行以上代码,执行结果为:
从执行时间能够看到,p2会等待p1的执行结果,而后再执行,从输出hi能够看到p1完成状态转变以后,传递给resolve(或者reject)的结果会传递给p2中的resolve
。
从上面的例子,咱们能够了解到then()方法是Promise实例的方法,即Promise.prototype
上的,它的做用是为Promise实例添加状态改变时的回调函数,这个方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。
那么then()方法的返回值是什么?then
方法会返回一个新的Promise实例(注意,不是原来那个Promise,原来那个Promise已经承诺过,此时继续then就须要新的承诺~~),这样的设计的好处就是可使用链式写法。
还有一个点,就是链式中的then
方法(第二个开始),它们的resolve
中的参数是什么?答案就是前一个then()中resolve的return
语句的返回值。
来一个示例:
var p1 = new Promise( (resolve, reject) => { setTimeout(() => resolve('p1'), 10); }); p1.then( ret => { console.log(ret); return 'then1'; }).then( ret => { console.log(ret); return 'then2'; }).then( ret => { console.log(ret); });
在node环境下执行,执行结果为:
catch()方法是Promise实例的方法,即Promise.prototype
上的属性,它实际上是.then(null, rejection)
的简写,用于指定发生错误时的回调。
这个方法其实很简单,在这里并不想讨论它的使用,而是想讨论的是Promise中的错误的捕抓和处理。
Promise对象的Error对象具备冒泡
性质,会一直向后传递,直到被捕获为止。也就是说,错误老是会被下一个catch语句捕获,示例代码以下:
var p = new Promise( (resolve, reject) => { setTimeout(() => resolve('p1'), 10); }); p.then( ret => { console.log(ret); throw new Error('then1'); return 'then1'; }).then( ret => { console.log(ret); throw new Error('then2'); return 'then2'; }).catch( err => { // 能够捕抓到前面的出现的错误。 console.log(err.toString()); });
执行结果以下
在第一个then中抛出了一个错误,在最后一个Promise对象中能够catch到这个错误。
由于有这种方便的错误处理机制,因此通常来讲不要在then方法里面定义reject状态的回调函数, 而是使用catch方法
跟传统的
try/catch
不一样的是,若是没有使用catch
方法指定错误处理回调函数,则Promise对象抛出的错误不会传递到外层代码(在chrome会报错)
Node.js有一个unhandledRejection事件,专门监听未捕获的reject错误。如下代码就是在node环境下运行。
var p = new Promise((resolve, reject) => { resolve(x + 2); }); p.then( () => { console.log('nothing'); });
没错,既然catch()是.then(null, rejection)
的别名,那么catch()就会返回一个Promise对象,所以在后面还能够接着调用then
方法,示例代码以下:
var p = new Promise((resolve, reject) => { resolve(x + 2); }); p.then( () => { console.log('nothing'); }).catch( err => { console.log(err.toString()); return 'catch'; }).then( ret => { console.log(ret); });
当出错时,catch会先处理以前的错误,而后经过return语句,将值继续传递给后一个then方法中。
若是没有报错,则跳过catch,示例以下:
var p = new Promise((resolve, reject) => { resolve('p'); }); p.then( ret => { console.log(ret); return 'then1'; }).catch( err => { console.log(err.toString()); return 'catch'; }).then( ret => { console.log(ret); });
Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例,例如
var p = Promise.all([p1, p2, p3]);
新的Promise实例p
的状态由p1, p2, p3
决定:
当p1, p2, p3
的状态都为完成态
时,p为完成态。
p1, p2, p3
中任一一个状态为失败态
,则p为失败态。
Promise.race方法一样是将多个Promise实例,包装成一个新的Promise实例。
var p = Promise.race([p1, p2, p3]);
不一样的是,只要p1, p2, p3
中任意一个实例率先改变状态,则p
的状态就跟着改变,并且状态由率先改变的实例决定。
var p = Promise.race([ new Promise(resolve => { setTimeout(() => resolve('p1'), 10000); }), new Promise((resolve, reject) => { setTimeout(() => reject(new Error('time out')), 10); }) ]); p.then( ret => console.log(ret)) .catch( err => console.log(err.toString()));
Promise.resolve()能够将现有的对象转为Promise对象。
var p = Promise.resolve('p'); // 至关于 var p = new Promise(resolve => resolve('p'));
比较有意思的是Promise.resolve()会根据参数类型进行相应的处理,分几种状况讨论。
参数是一个Promise实例,那么Promise.resolve将不作任何处理,直接返回这个实例。
参数是一个thenable
对象,也就是说对象是具备then
方法的对象,但不是一个Promise实例(就跟类数组和数组的关系同样),例如
let thenable = { then : function (resolve, reject) { resolve(42); } }; let p = Promise.resolve(thenable); p.then( ret => console.log(ret)); // 42
Promise.resolve方法会将这个对象转为Promise对象,而后当即执行thenable对象中的then方法,由于例子中的thenable对象的then方法中执行了resolve
,所以会输出结果42
。
若是参数是一个原始值,或者不具备then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为resolve
,而后直接将该参数传递给resolve
方法。
var p = Promise.resolve("p"); p.then( ret => console.log(ret)); // p
Promise.resolve方法不带参数时,会直接返回一个resolve
状态的Promise对象。
须要注意的当即resolve
的Promise对象,是在本轮事件循环
的结束时,而不是下一轮事件循环
的开始执行。示例代码:
setTimeout(() => console.log('3'), 0); var p = Promise.resolve(); p.then(() => console.log('2')); console.log('1');
输出结果为:
Promise.reject()返回一个新的Promise实例,该实例的状态为rejected
,对于传入的参数的处理跟Promise.resolve相似,就是状态都为rejected
。
Promise对象的回调链,无论以then方法或者catch方法结尾,要是最后一个方法抛出错误,都有可能没法捕捉到,由于Promise内部的错误不会冒泡到全局,所以,咱们能够提供一个done方法,老是处理回调链的尾端,保证抛出任何可能出现的错误。
这个代码的实现很是简单
Promise.prototype.done = function (resolve, reject) { this.then(resolve, reject) .catch( function (reason) { // 抛出一个全局错误 setTimeout( () => { throw reason }, 0); }); } // 使用示例 var p = new Promise( (resolve, reject) => { resolve('p'); }); p.then(ret => { console.log(ret); return 'then1'; }).catch( err => { console.log(err.toString()); }).then( ret => { console.log(ret); return 'then2'; }).then( ret => { console.log(ret); x + 2; }).done();
这里为何能够在全局抛出一个错误?缘由就是setTimeout中的回调函数是在全局做用域中执行的
,所以抛出的错误就是在全局做用域上。
finally方法用于指定无论Promise对象最后的状态如何,都会执行的操做,它与done方法最大的区别就是,它接受一个普通函数做为参数,该函数无论怎么样都必须执行。
Promise.prototype.finally = function (callback) { let P = this.constructor; return this.then( ret => P.resolve(callback()).then( () => ret), err => P.resolve(callback()).then( () => {throw reason }) ); };
从上面几个小节综合来看,能够看到Promise其实就是作了一件事情,那就是对异步操做进行了封装,而后能够将异步操做以同步的流程表达出来,避免了层层嵌套的回调函数,同时提供统一的接口,使得控制异步操做更加容易。
可是,Promise也有一些缺点:
没法取消Promise,一旦新建它就会当即执行,没法中途取消。
若是不设置回调函数,Promise内部的错误不会反应到外部。
当处于未完成态时,没法得知目前进展到哪个阶段。
使用Generator函数来管理流程,遇到异步操做的时候,一般返回一个Promise
对象。
function getFoo() { return new Promise( resolve => resolve('foo')); } var g = function * () { try { var foo = yield getFoo(); console.log(foo); } catch(e){} } function run(generator) { var it = generator(); function go(result) { if(result.done) return result.value; // 默认value是一个Promise,其实这里应该作判断的 if(!(result.value instanceof Promise)){ throw Error('yield must follow an instanceof Promise'); } return result.value.then( ret => go(it.next(ret)) ).catch(err => go(it.throw(err))); } go(it.next()); } run(g);
上面代码的Generator函数g之中,有一个异步操做getFoo,它返回的就是一个Promise对象。函数run用来处理这个Promise对象,并调用下一个next方法。