ECMAScript6 的 Promise 学习小结

Promise 是异步编程的一种解决方案,它代表了未来将要发生的事情。ES6 将 Promise 作为标准,对其提供了原生支持。使用 Promise 的好处在于可以抛弃原来层层嵌套的回调函数编程方式,改用链式调用去完成异步任务。听起来就很牛逼,先看一个例子,对比一下传统 Javascript 的写法与使用 Promise 的区别。

传统 Javascript 回调和 Promise 的对比

假设有3个异步任务,分别对应3个函数 step1step2step3,并且要求他们的执行顺序是上一个执行完返回结果后才执行下一个。这里以 setTimeout 延时来代替异步任务的执行。那么如果用传统 JavaScript 可能会这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function step1(callback) {
setTimeout(function() {callback();}, 1000);
}
function step2(callback) {
setTimeout(function() {callback();}, 2000);
}
function step3(callback) {
setTimeout(function() {callback();}, 3000);
}
// 执行3个任务
step1(function() {
step2(function() {
step3(function() {
// ...
});
});
});

看起来有点儿意思,回调嵌套金字塔成功引起了我们的注意,日后这里的代码可能会成为理解和维护的难点。
那么,如果用 Promise 来实现,代码会变成什么样?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function step1() {
return new Promise((resolve) => {
setTimeout(() => resolve(), 1000);
});
}
function step2() {
return new Promise((resolve) => {
setTimeout(() => resolve(), 2000);
});
}
function step3() {
return new Promise((resolve) => {
setTimeout(() => resolve(), 3000);
});
}
// 执行3个任务
step1().then(step2).then(step3);

看执行3个任务的调用方式,是不是明显比上面的回调嵌套更好理解?相比之下,这种链式调用读起来明显比回调金字塔容易。

Promise 对象的创建和状态改变

在 ECMAScript6 中,Promise 是一个类,通常我们使用 Promise 的构造函数来创建一个 Promise 对象。

1
2
// 创建一个 Promise 对象
var promise = new Promise((resolve, reject) => {});

注意,执行 new Promise 时,被当作参数传进去的函数会立即被调用,该函数有两个参数 resolvereject,这两个也都是函数,可以用来通知 promise 对象改变状态,并触发回调函数。

Promise 对象有3种状态:

  • Pending(初始状态)
  • Resolved(成功)
  • Rejected(失败)

promise 对象在任何时候都处于这3种状态中的某一种。而状态的改变只能有以下两种情况。

  • 调用 resolve 函数将状态由 Pending 改为 Resolved
  • 调用 reject 函数将状态由 Pending 改为 Rejected

而且状态一旦改变一次,就不会再次改变。

看一个改变 Promise 对象状态的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function asyncTask(num) {
return new Promise((resolve, reject) => {
// 此时 promise 的状态为 Pending
setTimeout(() => {
var success = num > 0; // 用传进来的参数是否大于0来模拟异步操作是否成功
if (success) {
resolve(num); // 此时 promise 的状态为 Resolved
} else {
reject(num); // 此时 promise 的状态为 Rejected
}
}, 2000);
});
}
var promise = asyncTask(10);

实例方法 promise.then(onResolve, onReject)

这是 promise 实例的方法,用来添加状态改变时的回调函数。该方法可以接收两个 function 作为参数,分别指定状态改变为 Resolved 和 Rejected 时的回调。而且这两个回调函数都可以接收参数,参数是由 resolve()reject() 函数被调用时传进去的。看个例子:

1
2
3
4
5
6
7
// 这里我们使用上面写的 getPromise() 方法获取一个 promise 对象
asyncTask(10).then(
// 状态变为 Resolved 时 onResolved 被调用
(data) => console.log('onResolved: ' + data),
// 状态变为 Rejected 时 onRejected 被调用
(data) => console.log('onRejected: ' + data)
);

注意在 new Promise 时传进去的函数执行过程中,如果某个步骤执行出错,那么 promise 对象会变成 Rejected 状态。并且一旦 promise 变为 Rejected 状态,接下来即使调用 resolve() 方法,也不会将状态变为 Resolved。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function getPromise() {
return new Promise((resolve, reject) => {
reject('error message');
console.log('some code');
resolve('resolved message');
});
}
getPromise().then(
(data) => console.log('onResolved: ' + data),
(data) => console.log('onRejected: ' + data)
);
// 输出:
// some code
// onRejected: error message

因为 promise.then() 方法返回一个 Promise 实例对象,所以可以继续调用其返回值的 then() 方法实现链式调用。并且在回调函数中的返回值会被传入到下一个 then() 方法的回调函数中。

1
2
3
4
5
6
7
8
asyncTask(10).then(
// 状态变为 Resolved 时 onResolved 被调用
(data) => data + ' > 0',
// 状态变为 Rejected 时 onRejected 被调用
(data) => data + ' <= 0'
).then((data) => console.log(data));
// 输出:
// 10 > 0

虽然 promise.then() 方法返回一个 Promise 实例对象,但应该注意,它返回的对象与调用 then() 方法的对象已经不是同一个了。看个例子:

1
2
3
4
5
var promise1 = asyncTask(10);
var promise2 = promise1.then();
promise1 == promise2;
// 输出:
// false

理解这一点,有助于理解在 then() 链式调用中发生了什么。先看个例子:

1
2
3
4
5
6
7
8
9
asyncTask(-1).then(
(data) => 'onResolved1 : ' + data,
(data) => 'onRejected1 : ' + data
).then(
(data) => console.log('onResolved2 : ' + data),
(data) => console.log('onRejected2 : ' + data)
);
// 输出:
// onResolved2 : onRejected1 : -1

注意输出结果是onResolved2 : onRejected1 : -1,而不是onRejected2 : onRejected1 : -1。因为虽然第一个 then() 调用的 Promise 对象处于 Rejected 状态,但是第二个 then() 调用的 Promise 对象跟前面不是同一个对象,所以它的状态不受前面的影响。根据输出结果中的onRejected2,我们可以断定 promise2 的状态是 Resolved。

在 then 方法的回调 onResolved 和 onRejected 中,可以通过返回值将值传递到下一个 then() 方法的回调中,也可以返回一个 Promise 对象。
以下两段代码有什么区别?

1
2
3
4
5
6
7
8
9
10
11
12
// Case A
Promise.resolve(100).then((data) => {
return new Promise((resolve, reject) => {
console.log('add1');
resolve(data + 1);
});
}).then((data) => console.log("data1 = " + data));
// Case B
Promise.resolve(100).then((data) => {
console.log('add2');
return data + 1;
}).then((data) => console.log('data2 = ' + data));

Case A 在第一个 then() 中返回了一个 Promise 对象,在改对象中用 resolve() 传入值。而 Case B 在第一个 then() 中直接将处理结果返回。看一下输出结果:

1
2
3
4
5
6
// Case A 的输出结果
add1
data1 = 101
// Case B 的输出结果
add2
data2 = 101

结果看起来没区别,难道这两种写法是等价的?如果加入 setTimeout,变成这样呢?

1
2
3
4
5
6
7
8
9
// Case A
Promise.resolve(100).then((data) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('add1');
resolve(data + 1);
}, 1000);
});
}).then((data) => console.log("data1 = " + data));

// 输出结果:1s 后输出:
add1
data1 = 101

1
2
3
4
5
6
7
// Case B
Promise.resolve(100).then((data) => {
setTimeout(() => {
console.log('add2');
return data + 1;
}, 1000);
}).then((data) => console.log('data2 = ' + data));

// 输出结果:
data2 = undefined
// 1s 后输出:
add2

看出区别了,加入异步代码后,Case A 正常执行,而 Case B 先执行了后面的回调,再执行前面的异步代码。由此可得出结论:

  1. 在链式调用 then() 方法时,如果执行的都是同步操作,那么 onResolved 回调中返回 Promise 对象或直接返回结果值,最终结果都一样。
  2. 如果执行的是异步操作,并希望在异步操作完成后将结果值继续往下传递,则应该在 onResolved 回调中返回一个 Promise 对象,并在其中处理异步代码,处理结果用 resolve() 继续往下传。

实例方法 promise.catch

这个方法其实就是 promise.then(null, reject) 方法的别名,用于发生错误时回调。一般不要在调用 then() 方法时传入 onRejected 回调函数(即第二个参数),而应该使用 catch() 方法。

1
2
3
4
5
6
7
8
9
10
// bad
promise.then(
(data) => { /* success */ },
(err) => {/* error */ }
);
// good
promise
.then((data) => { /* success */ })
.catch((err) => { /* error */ });

上面的代码中,第二种写法要好于第一种写法,因为第二种写法才可以捕获 then() 方法中的错误。

Promise.resolve() 和 Promise.reject()

这两个 Promise 的类方法都返回一个 Promise 对象,其实只是以下两个方法的别名而已

1
2
3
4
5
6
Promise.resolve = function (value) {
return new Promise((resolve, reject) => resolve(value));
};
Promise.reject = function (value) {
return new Promise((resolve, reject) => reject(value));
};

Promise.all() 方法

Promise.all() 接受一个 Promise 数组作为参数,返回一个 Promise 对象。如:

1
var p = Promise.all([p1, p2, p3]);

p 与 p1、p2、p3 具有以下关系:

  1. 只有 p1、p2、p3 都变为 Resolved 状态,p 才会变成 Resolved 状态
  2. 只要 p1、p2、p3 中某一个变为 Rejected 状态,p 就变成 Rejected 状态。

Promise.race() 方法

Promise.race() 方法与 Promise.all() 类似,接受一个 Promise 数组作为参数,返回一个 Promise 对象。如:

1
var p = Promise.race([p1, p2, p3]);

只要 p1、p2、p3 中任意一个改变了状态,p 对象的状态就跟着改变。且率先改变状态的 Promise 实例的返回值,会传递给 p 的回调函数。

以上