一、 什么是Promise
在《你不知道的JavaScript中》有个场景介绍得很形象:
我走到快餐店的柜台,点了一个芝士汉堡。我交给收银员1.47美元。通过下订单并付款,我已经发出了一个对某个值(就是那个汉堡)的请求。我已经启 动了一次交易。
但是,通常我不能马上就得到这个汉堡。收银员会交给我某个东西来代替汉堡:一张带有订单号的收据。订单号就是一个 IOU(I owe you, 我欠你的)承诺(promise),保证了最 终我会得到我的汉堡。
在等待的过程中,我可以做点其他的事情。事实上,订单号当作芝士汉堡的占位符了。从本质上讲,这个占位符使得这个值不再依赖时间。这是一个未来值。
终于, 我听到服务员在喊“订单 113” , 然后愉快地拿着收据走到柜台, 把收据交给收银 员,换来了我的芝士汉堡。
换句话说, 一旦我需要的值准备好了, 我就用我的承诺值(value-promise)换取这个值 本身。
但是,还可能有另一种结果。他们叫到了我的订单号,但当我过去拿芝士汉堡的时候,收银员满是歉意地告诉我:“不好意思,芝士汉堡卖完了。”除了作为顾客对这种情况感到愤怒之外,我们还可以看到未来值的一个重要特性:它可能成功,也可能失败。
每次点芝士汉堡,我都知道最终要么得到一个芝士汉堡,要么得到一个汉堡包售罄的坏消息,那我就得找点别的当午饭了。
所以Promise的出现其实是作为异步编程的一种解决方案。比传统的解决方案-回调函数和事件-更加合理、强大。Promise相当于异步操作结果的占位符,他不会去订阅一个事件,也不会传递一个回调函数给目标函数,而是让函数返回一个Promise(相当于订单号)。
二、 为什么使用Promise
当我们采用回调模式实现异步编程时,容易产生回调地狱,即在一个回调函数中嵌套多个回调函数,使得代码难以理解和调试,例如:
method1(function(err,result) {
if (err) {
throw err;
}
method2(function(err, result) {
if (err) {
throw err;
}
method3(function(err, result) {
if (err) {
throw err;
}
method4(function(err, result) {
if (err) {
throw err;
}
method5(result);
});
});
});
});
所以,如果你想实现更复杂的功能,回调函数的局限性就显现出来。例如,并行在执行两个异步操作,当操作都结束时通知你;或者同时进行两个异步操作,只取优先完成的操作结果。而Promise就能很好的实现这些情况。下面我们就来介绍一下Promise的基本用法。
三、 Promise的基本用法
1、promise的生命周期
每个 Promise都会经历一个短暂的生命周期,初始为挂起态( pending state),这表示异步操作尚未结束。一个挂起的 Promise 也被认为是未决的( unsettled )。一旦异步操作结束, Promise就会被认为是已决的( settled ),并进入两种可能状态之一:
1. 已完成(fulfilled ): Promise 的异步操作已成功结束;
2. 已拒绝(rejected ): Promise 的异步操作未成功结束,可能是一个错误,或由其他原因导致。
一旦状态改变,就「凝固」了,会一直保持这个状态,不会再发生变化。当状态发生变化,promise.then绑定的函数就会被调用。注意:Promise一旦新建就会「立即执行」,无法取消。这也是它的缺点之一。
2、创建未决的 Promise
新的 Promise使用 Promise 构造器来创建。此构造器接受单个参数:一个被称为执行器(executor)的函数,包含初始化Promise 的代码。该执行器会被传递两个名为 resolve()与 reject() 的函数作为参数。 resolve() 函数在执行器成功结束时被调用,用于示意该Promise 已经准备好被决议( resolved ),而 reject() 函数则表明执行器的操作已失败。
//构建Promise
var promise = newPromise(function (resolve, reject) {
if (/* 异步操作成功 */) {
resolve(data);
} else {
/* 异步操作失败 */
reject(error);
}
});
要记住执行器会在Promise被创建时立即运行。当 resolve() 或 reject() 在执行器内部被调用时,一个作业被添加到作业队列中,以便决议(resolve )这个 Promise 。这被称为作业调度(job scheduling ),若你曾用过 setTimeout() 或 setInterval() 函数,那么应该已经熟悉这种方式。在作业调度中,你添加新作业到队列中是表示:“不要立刻执行这个作业,但要在稍后执行它”。Promise 工作方式与 setTimeout() 相似,此处有个例子:
let promise = newPromise(function(resolve, reject) {
console.log("Promise");
resolve();
});
promise.then(function(){
console.log("Resolved.");
});
console.log("Hi!");
此例的输出结果为:
Promise
Hi!
Resolved
Promise的执行器会立即执行,早于源代码中在其之后的任何代码。调用 resolve()触发了一个异步操作。传递给 then() 与 catch() 的函数会异步地被执行,并且它们也被添加到了作业队列(先进队列再执行),将在当前脚本所有同步任务执行完才会执行。
3、then方法和catch方法
then()方法在所有的 Promise上都存在,并且接受两个参数。第一个参数是 Promise 被完成时要调用的函数,与异步操作关联的任何附加数据都会被传入这个完成函数。第二个参数则是 Promise 被拒绝时要调用的函数,与完成函数相似,拒绝函数会被传入与拒绝相关联的任何附加数据。
用这种方式实现 then()方法的任何对象都被称为一个 thenable 。所有的 Promise 都是thenable ,反之则未必成立。
传递给 then()的两个参数都是可选的,因此你可以监听完成与拒绝的任意组合形式。例如,研究这组 then() 调用:
let promise =readFile("example.txt");
promise.then(function(contents){
// 完成
console.log(contents);
}, function(err) {
// 拒绝
console.error(err.message);
});
promise.then(function(contents){
// 完成
console.log(contents);
});
promise.then(null,function(err) {
// 拒绝
console.error(err.message);
});
这三个 then()调用都操作在同一个 Promise 上。第一个调用同时监听了完成与失败;第二个调用只监听了完成,错误不会被报告;第三个则只监听了拒绝,并不报告成功信息。
Promis也具有一个 catch()方法,其行为等同于只传递拒绝处理函数给 then() 。例如,以下的 catch() 与 then() 调用是功能等效的。
promise.catch(function(err){
//拒绝
console.error(err.message);
});
// 等同于:
promise.then(null,function(err) {
//拒绝
console.error(err.message);
});
Promise的状态一经改变,就固定不变。所以,即使完成或拒绝处理函数在 Promise已经被解决之后才添加到作业队列,它们仍然会被执行。这允许你随时添加新的完成或拒绝处理函数,并保证它们会被调用。例如:
let promise =readFile("example.txt");
// 原始的完成处理函数
promise.then(function(contents){
console.log(contents);
//现在添加另一个
promise.then(function(contents){
console.log(contents);
});
});
在此代码中,完成处理函数又为同一个 Promise添加了另一个完成处理函数。这个 Promise此刻已经完成了,因此新的处理程序就被添加到任务队列,并在就绪时(前面的作业执行完毕后)被调用。拒绝处理函数使用同样方式工作。
4、串联 Promise每次对 then()或 catch() 的调用实际上创建并返回了另一个 Promise ,仅当前一个Promise 被完成或拒绝时,后一个 Promise 才会被决议。
let p1 = newPromise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value){
console.log(value);
}).then(function(){
console.log("Finished");
});
此代码输出:
42
Finished
对p1.then()的调用返回了第二个 Promise ,又在这之上调用了 then() 。仅当第一个Promise 已被决议后,第二个 then() 的完成处理函数才会被调用。
5、捕获错误
Promise链允许你捕获前一个 Promise的完成或拒绝处理函数中发生的错误
let p1 = newPromise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value){
throw newError("Boom!");
}).catch(function(error){
console.log(error.message);// "Boom!"
});
6、在Promise链中返回值
Promise链的另一重要方面是能从一个 Promise传递数据给下一个 Promise 的能力。传递给执行器中的 resolve() 处理函数的参数,会被传递给对应 Promise 的完成处理函数,这点你前面已看到过了。你可以指定完成处理函数的返回值,以便沿着一个链继续传递数据。例如:
let p1 = newPromise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value){
console.log(value);// "42"
return value + 1;
}).then(function(value){
console.log(value);// "43"
});
也可以在 Promise 链中返回 Promise:
let p1 = newPromise(function(resolve, reject) {
resolve(42);
});
let p2 = newPromise(function(resolve, reject) {
resolve(43);
});
p1.then(function(value){
// 第一个完成处理函数
console.log(value);// 42
return p2;
}).then(function(value){
// 第二个完成处理函数
console.log(value);// 43
});
在此代码中, p1安排了一个决议 42 的作业,p1 的完成处理函数返回了一个已处于决议态的 Promise : p2 。由于 p2 已被完成,第二个完成处理函数就被调用了。而若 p2 被拒绝,会调用拒绝处理函数(如果存在的话),而不调用第二个完成处理函数。
let p1 = newPromise(function(resolve, reject) {
resolve(42);
});
p1.then(function(value){
console.log(value);// 42
// 创建一个新的 promise
let p2 = newPromise(function(resolve, reject) {
resolve(43);
});
return p2
}).then(function(value){
console.log(value);// 43
});
在此例中,一个新的 Promise在 p1 的完成处理函数中被创建。这意味着直到 p2 被完成之后,第二个完成处理函数才会执行。若你想等待前面的 Promise 被解决,之后才去触发另一个 Promise ,那么这种模式就非常有用。
这样,我们就可以利用then进行「链式回调」,将异步操作以同步操作的流程表示出来。Promise的真正强大之处在于它的多重链式调用,可以避免层层嵌套回调。
7、创建已决的 Promise
Promise.resolve()
语法:
Promise.resolve(value);
Promise.resolve(promise);
Promise.resolve(thenable);
它可以看做new Promise()的快捷方式。
这段代码会让这个Promise对象立即进入resolved状态,并将结果success传递给then指定的onFulfilled回调函数。由于Promise.resolve()也是返回Promise对象,因此可以用.then()处理其返回值。
Promise.reject()
语法:Promise.reject(reason)
这个方法和上述的Promise.resolve()类似,它也是new Promise()的快捷方式。
这段代码会让这个Promise对象立即进入rejected状态,并将错误对象传递给then指定的onRejected回调函数。
Promise.resolve()的另一个作用就是将thenable对象(即带有then方法的对象)转换为promise对象。
let thenable = {
then:function(resolve, reject) {
resolve(42);
}
};
let p1 =Promise.resolve(thenable);
p1.then(function(value){
console.log(value);// 42
});
在此例中, Promise.resolve()调用了 thenable.then() ,确定了这个 thenable 的Promise 状态:由于 resolve(42) 在 thenable.then() 方法内部被调用,这个 thenable 的Promise 状态也就被设为已完成。一个名为 p1 的新 Promise 被创建为完成态,并从thenable 中接收到了值(此处为 42 ),于是 p1 的完成处理函数就接收到一个值为 42 的参数。
8、响应多个 Promise
ES6 提供了能监视多个 Promise 的两个方法:Promise.all() 与 Promise.race() 。
Promise.all() 方法
Promise.all() 方法接收单个可迭代对象(如数组)作为参数,并返回一个 Promise 。这个可迭代对象的元素都是 Promise ,只有在它们都完成后,所返回的 Promise 才会被完成。例如:
let p1 = new Promise(function(resolve, reject) {
resolve(42);
});
let p2 = new Promise(function(resolve, reject) {
resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
resolve(44);
});
let p4 = Promise.all([p1, p2, p3]);
p4.then(function(value) {
console.log(Array.isArray(value)); // true
console.log(value[0]); // 42
console.log(value[1]); // 43
console.log(value[2]); // 44
});
- 当p1, p2, p3状态都变为fulfilled,p的状态才会变为fulfilled,并将三个promise返回的结果,按参数的顺序(而不是resolved的顺序)存入数组,传给p的回调函数
- 当p1, p2, p3其中之一状态变为rejected,p的状态也会变为rejected,并把第一个被reject的promise的返回值,传给p的回调函数
- 这多个 promise 是同时开始、并行执行的,而不是顺序执行
var p1 = new Promise(function(resolve, reject) {
setTimeout(reject, 500, "one");
});
var p2 = new Promise(function(resolve, reject) {
setTimeout(resolve, 100, "two");
});
Promise.race([p1, p2]).then(function(value) {
console.log('resolve', value);
}, function(error) {
//not called
console.log('reject', error);
});
四、总结