1. 从宏观的角度初步了解Promise
Promise含义不是字面的“誓言”“承诺”之类,而是“先知”的意思。
人类的行为虽然可以并行,但有意识的思维活动却呈现出单线程的特性。
比如,普通人同一时间只能思考一个问题。在思考的过程中,有时会突然受到灵感的启发联想到其他的问题。
熟悉javascript的人也许早已意识到:
人类有意识层面的思维过程和js的执行过程有很多相通之处。
js沿着时间线执行,其间会插入定时器或回调。时间就是这个过程的轴心。
js的创造者当初也许就是为了尽可能简单地和人类思维契合起来,才会如此设计js这个语言。
然而现实世界是复杂的,js需要处理的问题也是复杂的。
在现实世界中,作为一个普通人,思维随着时间这一个维度,始终做着单线程运算。而运算的参数就是“每个当下”所能得到的信息之和。
普通人是相对“惬意”的,如果人类及环境都是“上帝”写的程序,那么这个“上帝”就没有那么“惬意”了。因为,“他”需要预先考虑到每个场景,每个人各种可能的演化。
作为一个程序员,对于所写的程序而言,某种程度上扮演着“上帝”的角色。(看到这里,是不是有些飘飘然的感觉)
但我们并没有超能力,我们的思维依然是单线程的。一不小心就掉进了“回调地狱”中。
这时我们需要一种神器,让我们具有“先知”般的能力。Promise就是这类神器的代表。
2. 什么是Promise
Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise
对象。
Promise
对象有以下两个特点。
Promise
对象代表一个异步操作,有三种状态:
Pending
(进行中)、
Resolved
(已完成,又称Fulfilled)和
Rejected
(已拒绝)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise
对象的状态改变,只有两种可能:从Pending
变为Resolved
和从Pending
变为Rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
如需要了解更多Promise的规范,请参阅 http://malcolmyu.github.io/malnote/2015/06/12/Promises-A-Plus/
3. ES6 中的Promise
先在控制台打印出Promise看看,console.dir(Promise)
由上图可知,Promise是一个函数,自己身上有all、race、reject、resolve等方法,原型上有then、catch等方法
ES6规定,Promise对象是一个构造函数,用来生成Promise实例。
创造了一个Promise实例:
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('执行完成');
var num = Math.random();
if(num>=0.5){
resolve(num);
} else{
reject('数字小于0.5');
}
}, 2000);
});
Promise新建后就会立即执行。
上面的代码中,设置了一个2s后执行的定时器。
2s以后输出“执行完成”,然后生成一个随机数,如果数字大于等于0.5,我们认为是“成功”了,调用resolve修改Promise的状态。否则我们认为是“失败”了,调用reject并传递一个参数,作为“失败”的原因。
resolve
函数的作用是,将Promise对象的状态从“未完成”变为“已解决”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;
reject
函数的作用是,将Promise对象的状态从“未完成”变为“拒绝”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
4. Promise的使用实例
下面是异步加载图片的例子。
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
var image = new Image();
image.onload = function() {
resolve(image);
};
image.onerror = function() {
reject(new Error('Could not load image at ' + url));
};
image.src = url;
});
}
上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。
Promise.prototype.then()
then方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。
从表面上看,Promise只是简化了层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。
function runAsync1(){
var p = new Promise(function(resolve, reject){
//做一些异步操作
setTimeout(function(){
console.log('执行完成1');
resolve('数据1');
}, 2000);
});
return p;
}
function runAsync2(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
console.log('执行完成2');
resolve('数据2');
}, 2000);
});
return p;
}
function runAsync3(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
console.log('执行完成3');
resolve('数据3');
}, 2000);
});
return p;
}
链式调用代码如下:
runAsync1()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return runAsync3();
})
.then(function(data){
console.log(data);
});
以上代码的输出为:
执行完成1
数据1
执行完成2
数据2执行完成3
数据3
Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
var promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.catch(function(error) {
console.log(error);
});
上面代码中,promise抛出一个错误,就被catch方法指定的回调函数捕获。
需要注意的是,catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。
var someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
上面代码运行完
catch
方法指定的回调函数,会接着运行后面那个
then
方法指定的回调函数。如果没有报错,则会跳过
catch
方法。
Promise.all()
Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。
下面是一个例子,其中 runAsync1、 runAsync1 和runAsync3上面的例子中已有定义
Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
以上代码的输出为:
执行完成1
执行完成2
执行完成3
["数据1"," 数据2","数据3"]
有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据。
Promise.race()
Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。
all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法race「谁跑的快,以谁为准执行回调」。
我们把上面runAsync1的延时改为1秒来看一下:
Promise
.race([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
console.log(results);
});
以上代码的输出为:
执行完成1
数据1
执行完成2
执行完成3
在then里面的回调开始执行时,runAsync2()和runAsync3()并没有停止,仍旧再执行。于是再过1秒后,输出了他们结束的标志。这个race有什么用呢?使用场景还是很多的,比如我们可以用race给某个异步请求设置超时时间,并且在超时后执行相应的操作,代码如下:
//请求某个图片资源
function requestImg(){
var p = new Promise(function(resolve, reject){
var img = new Image();
img.onload = function(){
resolve(img);
}
img.src = 'xxxxxx';
});
return p;
}
//延时函数,用于给请求计时
function timeout(){
var p = new Promise(function(resolve, reject){
setTimeout(function(){
reject('图片请求超时');
}, 5000);
});
return p;
}
Promise
.race([requestImg(), timeout()])
.then(function(results){
console.log(results);
})
.catch(function(reason){
console.log(reason);
});
requestImg函数会异步请求一张图片,我把地址写为"xxxxxx",所以肯定是无法成功请求到的。timeout函数是一个延时5秒的异步操作。我们把这两个返回Promise对象的函数放进race,于是他俩就会赛跑,如果5秒之内图片请求成功了,那么遍进入then方法,执行正常的流程。如果5秒钟图片还未成功返回,那么timeout就跑赢了,则进入catch,报出“图片请求超时”的信息。
补充
以上代码例子均来源于网络。
主要参考的文章:
http://malcolmyu.github.io/malnote/2015/06/12/Promises-A-Plus/
http://www.cnblogs.com/lvdabao/p/es6-promise-1.html
http://es6.ruanyifeng.com/#docs/promise
https://segmentfault.com/a/1190000000684654