基础介绍

Promise 对象用于一个异步操作的最终完成(或失败)及其结果值的表示。(简单点说就是处理异步请求。我们经常会做些承诺,如果我赢了你就嫁给我,如果输了我就嫁给你之类的诺言。这就是promise的中文含义:诺言,一个成功,一个失败。)

MDN-Promise

Promise表示一个异步操作的结果,与之进行交互的方式主要是then方法,该方法注册了两个回调函数,用于接收promise的终值或本promise不能执行的原因。

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var p = new Promise(function(resolve, reject){ // executor
if (Math.random() < 0.5) {
resolve('终值'); // fulfill
} else {
reject('据因'); // reject
}
});

p.then(
function(value){ // onFulfilled
console.log(value);
},
function(reason){ // onRejected
console.log(reason);
}
);

new Promise实例化一个Promise,其第一个构造参数executor是一个带有resolvereject两个参数的函数,executor函数在Promise构造函数返回新建对象前被调用,被传递resolvereject函数。resolvereject函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor内部通常会执行一些异步操作,一旦完成,可以调用resolve函数来将promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。

  • 接受(fulfill):指一个promise成功时进行的一系列操作,如状态的改变、回调的执行。虽然规范中用fulfill来表示接受,但在后世的promise实现多以resolve来指代之。
  • 拒绝(reject):指一个promise失败时进行的一系列操作。
  • 终值(eventual value):所谓终值,指的是promise被接受时传递给解决回调的值,由于promise有一次性的特征,因此当这个值被传递时,标志着promise等待态的结束,故称之终值,有时也直接简称为值(value)。
  • 据因(reason):也就是拒绝原因,指在promise被拒绝时传递给拒绝回调的值。

三个状态

三个状态:未被决议、完成、拒绝,决议完状态不再改变

  • pending: 等待态,移至执行态或拒绝态
  • fulfilled: 接受态,不能迁移至其他任何状态,必须拥有一个不可变的终值
  • rejected: 拒绝态,不能迁移至其他任何状态,必须拥有一个不可变的据因

这里的不可变指的是恒等(即可用 === 判断相等),而不是意味着更深层次的不可变(译者注:指当 value 或 reason 不是基本值时,只要求其引用地址相等,但属性值可被更改)。

promise

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
var p = new Promise(function(resolve, reject) {
resolve(42);
reject('reason')
});
p.then(function(v) {
console.log(v); // 42
});
p.then(function(v) {
console.log(v); // 还是42
});
p.catch(function(reason) {
console.log(reason); // 不会执行
});

变量p为一个已经决议的promise,它的决议状态(PromiseStatus)一直都是resolved,值一直未42

Then

一个promise必须提供一个then方法以访问其当前值、终值和据因。

1
promise.then(onFulfilled, onRejected)

参数

  • onFulfilled:当Promise变成接受状态(fulfilled)时,该参数被调用。该函数有一个参数,即接受的值。
  • onRejected:当Promise变成拒绝状态(rejected)时,该参数被调用。该函数有一个参数,即拒绝的原因。

返回值

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变为终态时的回调函数的参数是相同的。

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var p1 = new Promise(function(resolve, reject) { // executor
if (Math.random() < 0.5) {
resolve('终值'); // fulfill
} else {
reject('据因'); // reject
}
});

var p2 = p1.then(
function(value) { // onFulfilled
console.log(value);
return new Promise(function(resolve) {
resolve(42);
});
},
function(reason){ // onRejected
console.log(reason);
return 21;
}
);

p2.then(function(value) {
console.log(value); // 42 或是 21
}, function(reason) {
console.log(reason); // 不会执行
})
  • 如果onFulfilled不是函数且p1成功执行,p2必须成功执行并返回相同的值
  • 如果onRejected不是函数且p1拒绝执行,p2必须拒绝执行并返回相同的据因

如下p1成功执行,p2onFulfilled不是函数,p2返回相同的值:

1
2
3
4
5
6
7
8
9
10
11
12
var p1 = Promise.resolve('终值'); // 等价于 new Promise(function(resovle) { resovle('终值') });
var p2 = p1.then(
undefined,
function(reason){ // onRejected
console.log(reason);
return 21;
}
);

p2.then(function(value) {
console.log(value); // '终值'
});

catch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var p1 = Promise.resolve('终值');

p1.then(
undefined,
function(reason){ // onRejected
console.log(reason);
return 21;
}
);

// 和上面等价
p1.catch(
function(reason){ // onRejected
console.log(reason);
return 21;
}
);

resolve

构造参数executorresolve接受一个参数(和then中的回调函数返回值处理流程一样):

  • 传递的不为promisethenable,则返回一个决议成功的promise,结果为传入的参数
  • 传递的promisethenable,则结果和此对象相同
1
2
var p = new Promise(function(resovle) { resovle('终值') });
var p2 = new Promise(function(resovle) { resovle(p) }); // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: "终值"}

Promise.resolve

Promise.resolve提供一个简便的方法来得到接受的promise

1
2
3
Promise.resolve('终值');
// 和上面接近等价
new Promise(function(resovle) { resovle('终值') });

不同的是,如果Promise.resolve的入参promisethenable,则直接返回入参。

1
2
3
var a = new Promise(function(resovle) { resovle('终值') });
Promise.resolve(a) === a // true
new Promise(function(resovle) { resovle(a) }) === a // false

Promise.reject

类似Promise.reject是一个简便的方法来得到拒绝的promise

1
2
3
Promise.reject('终值');
// 和上面等价
new Promise(function(resovle, reject) { reject('据因') });

构造参数executorrejectPromise.reject接受一个参数,无论改参数是何种状态的promise还是普通值,都将其作为据因

其它细节

执行时间

【翻译】Promises/A+规范中对此解释的很清楚:

注1 这里的平台代码指的是引擎、环境以及promise的实施代码。实践中要确保onFulfilledonRejected方法异步执行,且应该在then方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。由于promise的实施代码本身就是平台代码(译者注: 即都是 JavaScript),故代码自身在处理在处理程序时可能已经包含一个任务调度队列。

译者注: 这里提及了macrotaskmicrotask两个概念,这表示异步任务的两种分类。在挂起任务时,JS引擎会将所有任务按照类别分到这两个队列中,首先在macrotask的队列(这个队列也被叫做task queue)中取出第一个任务,执行完毕后取出microtask队列中的所有任务顺序执行;之后再取macrotask任务,周而复始,直至两个队列的任务都取完。

两个类别的具体分类如下:

  • macro-task: script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
  • micro-task: process.nextTick, Promises(这里指浏览器实现的原生 Promise), Object.observe, MutationObserver

详见stackoverflow 解答这篇博客

【翻译】Promises/A+规范 注1

补充一句:在处理微任务microtask时,他们可以排队更多的微任务,这些微任务都将被逐个运行,直到微任务队列耗尽。然后在再取macrotask任务。

考虑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var p1 = new Promise(function(resovle) { // executor
console.log('resolved'); // 1. resolved
resovle([1, 2, 3]);
});

p1
.then(function(values) { // 第一个onFulfilled
console.log(values); // 3. [1, 2, 3]
return values.reduce(function(p, v) {
return p + v;
}, 0); // 求和
})
.then(function(sum) { // 第二个onFulfilled
console.log(sum); // 5. 6
throw new Error('据因');
})
.catch(function(reason) { // onRejected
console.log(reason); // 6. '据因'
});

p1.then(function () { // 第三个onFulfilled
console.log('第三个onFulfilled'); // 4. '第三个onFulfilled'
});

console.log('同步执行'); // 2. Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 3}

setTimeout(function() {
console.log('setTimeout'); // 7. setTimeout
});

序号表示控制台打印的顺序,可以看出executor是同步执行的,在console.log('同步执行')执行后, 第一个onFulfilled、第三个onFulfilled、第二个onFulfilledonRejected陆续执行,最后才是setTimeout被执行。

再考虑以下代码,下面的示例展示了Promise.all的异步(当传递的可迭代对象是空时,是同步的):

1
2
3
4
5
6
7
8
9
var p1 = Promise.resolve(3);
var p2 = 1337;
var p3 = Promise.all([p1, p2]);

console.log(p3); // 1. Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
console.log(Promise.all([])); // 2. Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: Array(0)}
setTimeout(function() {
console.log(p3); // 3. Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 3}
});

可以看出p3在即使p1p2都是被接受的情况下还是未决议状态。 当script(整体代码)执行完成后,引擎再会去处理p3的决议(microtask)。 所有的microtask都处理完成后,再执行下一个macrotask(setTimeout)。

resolve和reject只接受一个参数

  • 如果使用多个参数,第一个参数之后的所有参数都会被忽略
  • 如果没有参数,valuereason则为undefined

没有决议,永远被挂着,不会执行

1
2
3
4
5
6
7
new Promise((resolve, reject) => {

}).then((res) => {
console.log(res); // 不会执行
}, (res) => {
console.log(res); // 不会执行
});

被吞掉的异常

以下代码会造成ReferenceError,然而没有处理。

1
2
3
4
5
6
7
new Promise(function(resolve, reject) {
resolve(a.b); // 运行后会提示 Uncaught (in promise) ReferenceError: a is not defined
// reject('123'); // 提示 Uncaught (in promise) 123
})
.then(function(v) {
console.log(v);
});

被拒绝的promise如果没有catch处理控制台会提示错误信息,但是不影响代码段继续执行, 这就是被吞掉的异常。被吞掉的异常较难察觉,无法记录。因此推荐所有的promise最后都加上catch;

1
2
3
4
5
6
7
8
9
10
new Promise(function(resolve, reject) {
resolve(a.b); // 运行后会提示 Uncaught (in promise) ReferenceError: a is not defined
// reject('123'); // 提示 Uncaught (in promise) 123
})
.then(function(v) {
console.log(v);
})
.catch(function(err) {
console.error(err);
});

Promise.all([])

Promise.all返回一个promise,只有在传入Promise.all所有promise完成, 返回的promise才会完成,并且其终值为所有promise终值组成的数组。 如果有任何promise拒绝,你只会得到第一个拒绝promise据因。 这种模式被称为: 只有所有人都到齐,门才会开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var p1 = Promise.resolve(3);
var p2 = 1337; // 参数不是promise时, 表示解决
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.all([p1, p2, p3]).then(values => {
console.log(values); // [3, 1337, "foo"]
});

// 加入被拒绝的promise
var p4 = Promise.all([p1, p2, p3, Promise.reject(555)]);

setTimeout(function() {
console.log(p4); // Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 555}
});

Promise.race([])

对于Promise.race来说,返回的promise只取决于只有第一个被决议promise, 并且其结果和被决议promise相同。这种模式被称为门闩:第一个到达者,打开门闩。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var p1 = Promise.resolve(3);
var p2 = 1337; // 参数不是promise时, 表示解决
var p3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "foo");
});
Promise.race([p1, p2, p3]).then(value => {
console.log(value); // 3
});

// 加入被拒绝的promise
var p4 = Promise.race([p1, p2, p3, Promise.reject(555)]);
var p5 = Promise.race([Promise.reject(555), p1, p2, p3]);

setTimeout(function() {
console.log(p4); // Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 3}
console.log(p5); // Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 555}
});

异步链式流

then返回一个promise,可以链式调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function getLocation() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve({ latitude: 88, longitude: 30 });
// reject(new Error('获取失败'));
}, 1000)
});
}

function getNearShops(location) {
// 根据location查询附近的店
return new Promise(function() {
resolve([{ name: '五六面馆', id: 1, location }]);
// reject(new Error('获取失败'));
});
}

getLocation()
.then(getNearShops)
.then((nearShops) => {
console.log(nearShops);
})
.catch((err) => {
console.error(err); // log
});

更复杂的异步链式流

使用Promise我们能更清晰、更自信把功能拆分,不用担心回调被过多调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function confirm(content) {
return new Promise(function(resolve, reject) {
model.confirm(content, ({ ok }) => {
if (ok) {
resolve();
} else {
reject(new Error('用户取消'));
}
})
})
}

var isAuthorizedLocation = false;
function authorizeLocation() {
if (isAuthorizedLocation) {
return Promise.resolve();
}
return confirm('是否授权定位') // 请求用户授权
.then(() => {
return new Promise(function(resolve, reject) { // 授权
// 去授权
resolve();
reject(new Error('授权失败'));
});
});
}

authorizeLocation() // 1. 授权
.then(getLocation) // 2. 获取定位
.then(getNearShops) // 3. 请求附近的店
.then((nearShops) => {
console.log(nearShops);
})
.catch((err) => {
console.error(err); // log
});

注意:如果Promise.race被传入空数组会被挂住,永远不会被决议,而Promise.all会立即完成。

参考