Promise是一种异步编程的解决方案,比起传统的回调函数方式,Promise要更合理和强大。
基本调用方式
下面直接给出代码,通过代码对Promise对象进行讲解和分析:
构造一个Promise实例的模版代码:
var promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});
首先,Promise在ES6中是一个构造函数,用来生成Promise实例。所以,当我们需要Promise对象的时候,直接用new Promise(…)这样的方式即可。
我们调用new Promise方法的时候,它接受一个函数作为参数,而这个函数的两个参数分别是resolve, reject(我们现在并不用关心这个函数以及它的参数是从何而来的,我们首先只要按照这样的格式书写代码并会用就可以了),resolve和reject这两个参数其实也都是系统提供的函数,我们可以在这个函数中(也就是如上代码中// … some code的后面)的适当时机(比如:异步调用成功/失败后)可以直接调用它们。
调用后,Promise对象就会自动的触发then方法,如果我们调用了resolve方法,表示异步调用成功,那么就会执行作为then方法第一个参数的函数;如果我们调用了reject方法,表示异步调用失败,那么就会执行作为then方法第二个参数的函数。
补充一点:Promise对象有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled)和Rejected(已失败)。调用resolve方法可以使Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),而reject则将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected)。这两个函数都可以传递参数value或error,将可以将异步操作的结果传递出去,我们可以在then方法的参数(其实也是一个函数)中取到这个value或error。
另外,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;
});
}
这个栗子只写了第一段代码模版的第一部分,在这段代码中,我们就能清楚的看到何时以及如何调用resolve方法和reject方法:在图片加载成功的回调函数中调用了resolve,在图片加载失败的回调函数中调用了reject。
resolve参数的另一种可能
在前文中我们已经知道了:如果调用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的状态是Pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是Resolved或者Rejected,那么p2的回调函数将会立刻执行。
栗子:
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
上面代码中,Promise实例p1在3秒之后变为rejected。而p2的状态在1秒之后改变,但是resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,才能导致触发catch方法指定的回调函数。
关于then方法的链式调用
then方法定义在Promise.prototype上,也就是说,是Promise实例的方法。因此,如果then方法中返回一个新的Promise实例,那么,then方法就可以被链式的调用,事实上,也正是如此;但是需要注意,then方法中返回的Promise已经不是调用then的那个Promise对象了,它是一个全新的promise对象。
例如像这样:
注:then方法中,第二个参数(对错误的处理函数)可以省略。
getJSON("/posts.json").then(function(json) {
return json.message;
}).then(function(message) {
// ...
});
由于getJSON返回的是一个Promise对象,那么它后面的then函数就会等待这个Promise对象的状态发生变化后被执行。而在这个then函数中,返回的json.message并不是一个异步操作,那么,它下面的一个then(第二个then)就会直接被调用。而如果,在这个then函数中返回的是一个promise对象,那么第二个then函数就会等到这个promise完成后(状态变为resolve或者reject)执行。
在下面这个栗子中,几个then函数会接连被调用,依次显示hello, world, !。
function printHello (ready) {
return new Promise(function (resolve, reject) {
if (ready) {
resolve("Hello");
} else {
reject("Good bye!");
}
});
}
function printWorld () {
alert("World");
}
function printExclamation () {
alert("!");
}
printHello(true)
.then(function(message){
alert(message);
})
.then(printWorld)
.then(printExclamation);
catch方法
Promise.prototype.catch方法是.then(null, rejection)的别名,(也就是then方法调用时省略第一个参数),它用于指定发生错误时的回调函数。
p1.then(function(posts) {
// ...
}).catch(function(error) {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
在上面这个栗子中,假设p1是一个已经定义过的promise对象,当p1中异步操作执行完毕,如果p1状态变为resolve,就会触发第一个then方法;如果p1状态变为reject,则会触发后面的catch方法(因为promise的错误具有冒泡的性质,会一直向后传递,直到被捕获)。同时,如果p1的then方法出现了错误,也会被catch函数捕获。
同时,由于catch函数实际上是then(null,rejection)的别名,所以在catch后,还可以继续链式调用then和catch函数。
由于Promise的错误具有冒泡的性质,于是建议大家总是使用catch,而不要在then方法中定义reject时的回调函数:
// 不推荐这种写法,这样在then的第一个函数参数中出现的错误无法被捕获
promise1
.then(function(data) {
// success
}, function(err) {
// error
});
// 推荐的方法,这种情况下,错误一定能被捕获
promise2
.then(function(data) {
// success
})
.catch(function(err) {
// error
});
另外,在非chrome浏览器中,promise对象中出现的错误不会被传递到外层代码。