前言
在Promise之前,js的异步编程都是采用回调函数和事件的方式。但是这种编程方式在处理复杂业务的情况下,很容易出现callback hell(回调地狱),使得代码很难被理解和维护。Promise就是改善这种情形的异步编程的解决方案,它由社区最早提出和实现,es6将其写进了语言标准,统一了用法,并且提供了一个原生的对象Promise。
但是在这之前,大家想要使用Promise,一般会借助于第三方库,或者当你知道其中的原理以后,也可以手动实现一个简易的Promise.当然,为了防止不可预知的bug,在生产项目中最好还是不要使用原生的或者自己编写的Promise(目前为止并不是所有浏览器都能很好的兼容ES6),而是使用已经较为成熟的有大量小伙伴使用的第三方Promise库。现今流行的各大js库,几乎都不同程度的实现了Promise,如jQuery、Zepto等,只是暴露出来的大都是Deferred对象,当然还有angularJs中的$q。
注:以下所有的测试代码请在高级浏览器或node环境下运行
我们还是先来看看Promise的真身
console.log(new Promise(
function(resolve,reject){ })
);
打开浏览器的控制台我们可以看到:
从控制台输出的Promise对象我们可以清楚的看到Promise对象有以下几种基本方法:
Promise.resolve()
Promise.reject()
Promise.all()
Promise.race()
Promise.prototype.then()
Promise.prototype.catch()
更多点击Promise官网API,我们先不着急记住他们,只要对Promise对象有个整体的认识。
1.Promise对象状态
- pending: 初始状态, 既不是 fulfilled 也不是 rejected.
- fulfilled: 成功的操作.
- rejected: 失败的操作.
pending状态的Promise对象既可转换为带着一个成功值的fulfilled状态,也可变为带着一个失败信息的 rejected状态。当状态发生转换时,Promise.then绑定的方法就会被调用。(当绑定方法时,如果 Promise对象已经处于fulfilled或rejected状态,那么相应的方法将会被立刻调用,所以在异步操作的完成情况和它的绑定方法之间不存在竞争条件。)
因为Promise.prototype.then和Promise.prototype.catch方法返回Promises对象, 所以它们可以被链式调用。
注意,Promise状态的改变只会出现从未完成态向完成态或失败态转化,不能逆反。完成态和失败态不能互相转化,而且,状态一旦转化,将不能更改。
2.基本用法
(1)constructor
语法
new Promise(executor);
new Promise(function(resolve, reject) { ... });
参数
name | desc |
---|---|
executor | 带有resolve、reject两个参数的函数对象。第一个参数用在处理执行成功的场景,第二个参数则用在处理执行失败的场景。一旦我们的操作完成即可调用这些函数。 |
ES6 的 Promise 对象是一个构造函数,用来生成 Promise 实例。
var promise = new Promise(function(resolve, reject) {
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});
上面代码中,Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 方法和 reject 方法。如果异步操作成功,则用 resolve 方法将 Promise 对象的状态,从“未完成”变为“成功”(即从 pending 变为 resolved);如果异步操作失败,则用 reject 方法将 Promise 对象的状态,从“未完成”变为“失败”(即从 pending 变为 rejected)。
Promise 实例生成以后,可以用 then 方法分别指定 resolve 方法和 reject 方法的回调函数。
这里我们要特别注意两点:
1⃣️ Promise 新建后就会立即执行。
我们来一段代码测试一下:
var p1=new Promise(function(res,rej){
setTimeout(()=>{res("p1 end!");},1200);
console.log('我先执行');
})
p1.then(function(data){
console.log(data);
})
控制台输出:
我先执行
p1 end!
2⃣️ 如果调用 resolve 方法和 reject 方法时带有参数,那么它们的参数会被传递给回调函数。reject 方法的参数通常是 Error 对象的实例,表示抛出的错误;resolve 方法的参数除了正常的值以外,还可能是另一个 Promise 实例,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作,比如像下面这样:
var p1 = new Promise(function(resolve, reject){
// ...
});
var p2 = new Promise(function(resolve, reject){
// ...
resolve(p1);
})
注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000);
console.log('p1p1p1');
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000);
console.log('p2p2p2');
})
p2
.then(result => console.log(result))
.catch(error => console.log(error));
执行结果如下:
这里也再次说明第一个注意点提到的Promise 新建后就会立即执行;
(2)Promise.prototype.then()
在实例化一个Promise对象之后,我们调用该对象实例的then()方法为实例添加状态改变时的回调函数:
- 第一个参数(函数)是resolved状态的回调函数
- 第二个参数(函数)是rejected状态的回调函数
我们做一个异步加载图片实际案例来小试牛刀
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://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1510427875428&di=0df1bd50978bba9e0c0ba75e325b956a&imgtype=0&src=http%3A%2F%2Fimg.zybus.com%2Fuploads%2Fallimg%2F141110%2F1-141110163F6.jpg"); //来自百度的梅西图片
loadImage1.then(function success() {
console.log("loadImage1 load success");
}, function fail() {
console.log("loadImage1 load fail");
});
var loadImage2 = loadImageAsync("http://127.0.0.1/upload/patty.png"); //这张图片不存在
loadImage2.then(function success() {
console.log("loadImage2 load success");
}, function fail() {
console.log("loadImage2 load fail");
});
我们看看控制台发生了什么:
返回值
Promise.prototype.then 方法返回的是一个新的Promise对象,因此可以采用链式写法,即then方法后面再调用另一个then方法。
then方法返回的新的Promise对象的行为与then中的回调函数的返回值有关:
- 如果then中的回调函数返回一个值,那么then返回的Promise将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
- 如果then中的回调函数抛出一个错误,那么then返回的Promise将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
- 如果then中的回调函数返回一个已经是接受状态的Promise,那么then返回的Promise也会成为接受状态,并且将那个Promise的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
- 如果then中的回调函数返回一个已经是拒绝状态的Promise,那么then返回的Promise也会成为拒绝状态,并且将那个Promise的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
- 如果then中的回调函数返回一个未定状态(pending)的Promise,那么then返回Promise的状态也是未定的,并且它的终态与那个Promise的终态相同;同时,它变为终态时调用的回调函数参数与那个Promise变为终态时的回调函数的参数是相同的。
是不是看着有一种迷糊的感觉,我们这里只看第一种情况。
var p2 = new Promise(function(resolve, reject) {
resolve(1);
});
p2.then(function(value) {
console.log(value); //1
return value + 1; //then中的回调函数返回一个值
}).then(function(value) {
console.log(value); // 2
});
这里有一点我们必须了解:
如果前一个回调函数返回的是Promise对象,这时后一个回调函数就会等待该Promise对象有了运行结果,才会进一步调用。
(3)Promise.prototype.catch()
Promise.prototype.catch方法是Promise.prototype.then(null, rejection)的别名,用于指定发生错误时的回调函数。这一点官方api中说的很清楚。
catch 方法可以用于您的promise组合中的错误处理,这个方法其实很简单,在这里并不想讨论它的使用,而是想讨论的是Promise中的错误的捕抓和处理。
catch返回的Promise状态参考上面then的返回值
例如:
var p = new Promise( (resolve, reject) => {
setTimeout(() => resolve('p'), 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());
});
//执行结果如下:
// p
// Error: then1
Promise对象的Error对象具有传递性,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。
当出错时,catch会先处理之前的错误,然后通过return语句,将值继续传递给后一个then方法。我们在上面例子的catch语句后再添加一个then语句,看看会出现什么结果。
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());
return 'err next';
}).then(data=>{
console.log(data);
});
//执行结果为
//p1
// Error: then1
//err next
注意!!!这里有个陷阱等着你往下跳
Promise的错误处理是一种绝望的设计。默认情况下,它假定你想让所有的错误都被Promise的状态吞掉,而且如果你忘记监听这个状态,错误就会默默地凋零/死去。
这时你可能会想到把catch语句写在Promise链最后面来解决,像这样:
.......
p.then( ret => {
console.log(ret);
throw new Error('then1');
return 'then1';
}).then( ret => {
console.log(ret);
throw new Error('then2');
return 'then2';
}).catch( errors => {
// 可以捕抓到前面的出现的错误。
console.log(errors.toString());
});
要是catch里面函数本身也有错误呢?谁来捕获它?还有一个没人注意的promise:catch(..)返回的promise,我们没有对它进行捕获,也没注册拒绝处理器。仅仅将另一个catch(..)贴在链条末尾,悬挂着一个困在未被监听的Promise中的,未被捕获的错误,即便这种可能性大大减少。
-
处理未被捕获的错误
Promise应当增加一个done(..)方法,它实质上标志着Promise链的“终结”。done(..)不会创建并返回一个Promise,所以传递给done(..)的回调很明显地不会链接上一个不存在的Promise链,并向它报告问题。done(..)的拒绝处理器内部的任何异常都作为全局的未捕获错误抛出(基本上扔到开发者控制台),这就和try catch(){ }差不多了。
done()方法我们下一次再介绍,关于Promise对象先讲到这里,下一次我们继续一起学习Promise。
谢谢观看!!!