Promise是ES6,async/await是ES7确定的ECMA标准.目前最新的 Chrome浏览器和Typescript已经支持.

Promiseasync/await极大的改善了JS的编码体验.本文将介绍它们使用方式和注意事项.

Promise

Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值.

它的基础语法:

1
new Promise( function(resolve, reject) {...} /* executor */  );

Promise基本的使用方式是用它构建一个Promise实例. 它接受一个函数作为参数,参数默认会传递两个值:reslovereject.这个两个参数是用来修改Promise的状态.

Promise只有三种状态:

  • pending: 初始状态,既不是成功,也不是失败状态。
  • fulfilled: 意味着操作成功完成。
  • rejected: 意味着操作失败。

⚠️fulfilled经常被 resolve代替.

pending 状态的 Promise 对象可能会变为fulfilled 状态并传递一个值给相应的状态处理方法,也可能变为失败状态(rejected)并传递失败信息。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。

Promise状态转换过程如下:

⚠️ Promise.prototype.thenPromise.prototype.catch 方法返回promise 对象, 所以它们可以被链式调用。

一个实例后的Promise可以通过Promise.prototype.thenPromise.prototype.catch获取到异步执行的结果

Promise原型

Promise.prototype.constructor

返回被创建的实例函数. 默认为 Promise 函数.

Promise.prototype.catch(onRejected)

添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的promise。当这个回调函数被调用,新 promise 将以它的返回值来resolve,否则如果当前promise 进入fulfilled状态,则以当前promise的完成结果作为新promise的完成结果.

Promise.prototype.then(onFulfilled, onRejected)

添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来resolve.

Promise.prototype.finally(onFinally)

添加一个事件处理回调于当前promise对象,并且在原promise对象解析完毕后,返回一个新的promise对象。回调会在当前promise运行完毕后被调用,无论当前promise的状态是完成(fulfilled)还是失败(rejected)

Promise方法

Promise.all(iterable)

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

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

上面代码中,Promise.all()方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

Promise.allSettled(iterable)

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。返回一个promise,该promise在所有promise完成后完成。并带有一个对象数组,每个对象对应每个promise的结果。

1
2
3
4
5
6
7
8
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];

await Promise.allSettled(promises);
removeLoadingIndicator();

上面代码对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失。

Promise.any(iterable)

Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected`状态。

1
2
3
4
5
6
7
8
9
10
11
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
console.log(first);
} catch (error) {
console.log(error);
}

上面代码中,Promise.any()方法的参数数组包含三个 Promise 操作。其中只要有一个变成fulfilledPromise.any()返回的 Promise 对象就变成fulfilled。如果所有三个操作都变成rejected,那么await命令就会抛出错误。

Promise.race(iterable)

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

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

上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

Promise.reject(reason)

返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法

1
2
3
4
5
6
7
8
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))

p.then(null, function (s) {
console.log(s)
});
// 出错了

Promise.resolve(value)

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

1
2
3
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))

手写 Promise

同时,需要在 myPromise的原型上定义链式调用的 then方法:

⚠️根据上小节介绍的.应该原型上应该还需要实现catch方法.并且thencatch应该返回一个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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
const PENDING ="pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected" ;

function Promise ( excutor ) {
let that = this ; // 缓存当前promise实例对象
that.status = PENDING ; // 初始状态
that . value = undefined ; // fulfilled状态时 返回的信息
that.reason = undefined; // rejected状态时 拒绝的原因
that.onFulfilledCallbacks = []; // 存储fulfilled状态对应的onFulfilled函数
that.onRejectedCallbacks = []; // 存储rejected状态对应的onRejected函数
function resolve(value) { // value成功态时接收的终值
if(value instanceof Promise) {
return value.then(resolve, reject);
}
// 实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。
setTimeout (() => {
// 调用resolve 回调对应onFulfilled函数
if ( that . status === PENDING ) {
// 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
that.status = FULFILLED ;
that.value = value ;
that.onFulfilledCallbacks.forEach( cb => cb (that.value));
}
});
}

function reject(reason) { // reason失败态时接收的拒因
setTimeout(()=>{
// 调用reject 回调对应onRejected函数
if(that.status===PENDING){
// 只能由pending状态 => fulfilled状态 (避免调用多次resolve reject)
that.status=FULFILLED;
that.value=value;
that.onFulfilledCallbacks.forEach(cb=>cb(that.value));
}
});
}


// 捕获在excutor执行器中抛出的异常
// new Promise((resolve, reject) => {
// throw new Error('error in excutor')
// })
try{
excutor(resolve,reject);
} catch(e) {
reject(e);
}
}

Promise.prototype.then=function(onFulfilled,onRejected){
const that=this;
let newPromise;
// 处理参数默认值 保证参数后续能够继续执行
onFulfilled=typeof onFulfilled==="function"?onFulfilled:value=>value;
onRejected=typeof onRejected==="function"?onRejected:reason=>{throwreason;};
if ( that.status === FULFILLED ) { // 成功态
return newPromise = new Promise (( resolve , reject ) => {
setTimeout (() => {
try {
let x = onFulfilled(that.value);
resolvePromise (newPromise,x ,resolve,reject); // 新的promise resolve 上一个onFulfilled的返回值
} catch (e) {
reject (e); // 捕获前面onFulfilled中抛出的异常 then(onFulfilled, onRejected);
}
});
})
}
if (that.status === REJECTED ) { // 失败态
return newPromise = new Promise (( resolve , reject ) => {
setTimeout (() => {
try {
let x = onRejected (that.reason);
resolvePromise(newPromise,x ,resolve,reject);
} catch (e) {
reject (e);
}
});
});
}
if (that.status === PENDING ) { // 等待态
// 当异步调用resolve/rejected时 将onFulfilled/onRejected收集暂存到集合中
return newPromise = new Promise ((resolve,reject) => {
that.onFulfilledCallbacks.push((value) => {
try { let x = onFulfilled ( value );
resolvePromise(newPromise,x,resolve,reject);
} catch (e) {
reject (e);
}
});
that.onRejectedCallbacks.push((reason ) => {
try {
let x = onRejected (reason);
resolvePromise(newPromise,x,resolve,reject );
} catch ( e ) {
reject ( e );
}
});
});
}
}

async/await

async/await 简单来说,它们是基于promises的语法糖,使异步代码更易于编写和阅读。通过使用它们,异步代码看起来更像是老式同步代码

async

一个async修饰的函数,它会自动返回一个Promise实例

1
2
3
4
5
6
7
8
9
10
11
function hello() { return "Hello" };
hello(); // Hello

async function hello() { return "Hello" };
hello(); // Promise{<fulfilled>:unedfined}

//你也可以这样写:
let hello = async function() { return "Hello" };
hello();

let hello = async () => { return "Hello" };

Promise实例是通过then获取异步返回值的.所以可以这么写:

1
2
3
4
hello().then((value) => console.log(value))
// 还可以简写成:

hello().then(console.log)

async 关键字加到函数申明中,可以告诉它们返回的是 promise,而不是直接返回值。此外,它避免了同步函数为支持使用 await 带来的任何潜在开销。在函数声明为 async 时,JavaScript引擎会添加必要的处理,以优化你的程序。

await

await 关键字与异步函数一起使用时,它的真正优势就变得明显了 —— 事实上, await 只在异步函数里面才起作用。它可以放在任何异步的,基于 promise 的函数之前。它会暂停代码在该行上,直到 promise 完成,然后返回结果值。在暂停的同时,其他正在等待执行的代码就有机会执行了。

⚠️您可以在调用任何返回Promise的函数时使用 await,包括Web API函数。

1
2
3
4
5
async function hello() {
return greeting = await Promise.resolve("Hello");
};

hello().then(alert);

利用async/await重写Promise

使用Promise写的异步:

1
2
3
4
5
6
7
8
9
10
11
fetch('coffee.jpg')
.then(response => response.blob())
.then(myBlob => {
let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});

看起来和 使用回掉函数处理异步的写法很相像.

使用async/awaitPromise异步:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function myFetch() {
let response = await fetch('coffee.jpg');
let myBlob = await response.blob();

let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
}

myFetch()
.catch(e => {
console.log('There has been a problem with your fetch operation: ' + e.message);
});

myFetch内部看起来就像写同步函数一样.它使代码简单多了更容易理解 —— 去除了到处都是 .then() 代码块!

async/await处理异常

Promisereject状态是无法在 await获取到的. 正确的处理方式是,你应该用try...catch来处理可能出现的异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function myFetch() {
try {
let response = await fetch('coffee.jpg');
let myBlob = await response.blob();

let objectURL = URL.createObjectURL(myBlob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
} catch(e) {
console.log(e);
}
}

myFetch();

或者在外部函数 添加catch方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function myFetch() {
let response = await fetch('coffee.jpg');
return await response.blob();
}

myFetch().then((blob) => {
let objectURL = URL.createObjectURL(blob);
let image = document.createElement('img');
image.src = objectURL;
document.body.appendChild(image);
})
.catch((e) =>
console.log(e)
);

这是因为 .catch() 块将捕获来自异步函数调用和promise链中的错误。如果您在此处使用了try/catch 代码块,则在调用 myFetch() 函数时,您仍可能会收到未处理的错误。

async/await的缺陷

Async/await 让你的代码看起来是同步的,在某种程度上,也使得它的行为更加地同步。 await 关键字会阻塞其后的代码,直到promise完成,就像执行同步操作一样。它确实可以允许其他任务在此期间继续运行,但您自己的代码被阻塞。

这意味着您的代码可能会因为大量await的promises相继发生而变慢。每个await都会等待前一个完成,而你实际想要的是所有的这些promises同时开始处理(就像我们没有使用async/await时那样)。