一、讲讲Promise
在讲async
和await
之前,我们先来了解一下Promise
.
- Promise的语法:
new Promise( function(resolve, reject) {...} /* executor */ );
executor
是带有 resolve
和 reject
两个参数的函数 。Promise构造函数执行时立即调用executor
函数, resolve
和 reject
两个函数作为参数传递给executor
(executor 函数在Promise构造函数返回新建对象前被调用)。resolve
和 reject
函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作,一旦完成,可以调用resolve函数来将promise状态改成fulfilled,或者在发生错误时将它的状态改为rejected。
如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略。
- 描述
Promise
对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。 这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象
一个 Promise
有以下几种状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
pending 状态的 Promise 对象可能触发fulfilled 状态并传递一个值给相应的状态处理方法,也可能触发失败状态(rejected)并传递失败信息。当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法, 所以在异步操作的完成和绑定处理方法之间不存在竞争)。
因为 Promise.prototype.then
和 Promise.prototype.catch
方法返回promise 对象, 所以它们可以被链式调用。
- 创建Promise
Promise
对象是由关键字 new
及其构造函数来创建的。该构造函数会把一个叫做“处理器函数”(executor function)的函数作为它的参数。这个“处理器函数”接受两个函数——resolve
和 reject
——作为其参数。当异步任务顺利完成且返回结果值时,会调用 resolve
函数;而当异步任务失败且返回失败原因(通常是一个错误对象)时,会调用reject
函数。
const myFirstPromise = new Promise((resolve, reject) => {
// 做一些异步操作,最终会调用下面两者之一:
//
// resolve(someValue); // fulfilled
// 或
// reject("failure reason"); // rejected
});
想要某个函数拥有promise功能,只需让其返回一个promise即可。
function myAsyncFunction(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onload = () => resolve(xhr.responseText);
xhr.onerror = () => reject(xhr.statusText);
xhr.send();
});
};
二、谈谈async和await
- async返回一个Promise对象
async function testAsync() {
return "hello async";
}
const result = testAsync();
console.log(result); //输出的是一个 Promise 对象:Promise { 'hello async' }
所以,async 函数返回的是一个 Promise 对象。如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then()
链来处理这个 Promise 对象,就像这样:
testAsync().then(v => {
console.log(v); // 输出 hello async
});
联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。
- await等待一个表达式
await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。
因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行
function getSomething() {
return "something";
}
async function testAsync() {
return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2); // something hello async
}
test();
await——这个关键字只能够在 async 函数中使用,否则将会报错,它的意思是紧跟在其后面的表达式需要被等待执行结果。
await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。
如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果会自动转换成状态为resolve的Promise,那就是它等到的东西。
如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。如果await后面的Promise状态转变成了reject,那么整个 async 函数都会停止执行,并且抛出相应的错误。即使这里没有return,也一样可以传入错误回调的参数。
所以当一个 async 函数中有多个 await命令时,如果不想因为一个出错而导致其与的都无法执行,应将await放在try...catch
语句中执行
async function myFunction () {
try {
await func1()
await func2()
await func3()
} catch (error) {
console.log(error)
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise().catch(function (err){
console.log(err);
});
}
三、优化async&await的使用
当一个 async 函数中有多个await时,这些 await是继发执行的,只有当前一个await后面的方法执行完毕后,才会执行下一个
如果我们前后的方法有依赖关系,继发执行是没有问题的,但是如果并没有任何关系的话,这样就会很耗时,所以需要让这些await命令同时执行,也就是并发执行
。
// 方法 1
let [res1, res2] = await Promise.all([func1(), func2()])
// 方法 2
let func1Promise = func1()
let func2Promise = func2()
let res1 = await func1Promise
let res2 = await func2Promise // func1,func2同时执行,但是res2需要等待res1的结果返回
另外的场景,两对回调:
a(() => {
b();
});
c(() => {
d();
});
如果写成下面的方式,虽然一定能保证功能一致,但变成了最低效的执行方式:
await a();
await b();
await c();
await d();
因为翻译成回调,就变成了:
a(() => {
b(() => {
c(() => {
d();
});
});
});
然而我们发现,原始代码中,函数 c 可以与 a 同时执行,但 async/await 语法会让我们倾向于在 b 执行完后,再执行 c。
所以当我们意识到这一点,可以优化一下性能:
const resA = a();
const resC = c();
await resA;
b();
await resC;
d();
但其实这个逻辑也无法达到回调的效果,虽然 a 与 c 同时执行了,但 d 原本只要等待 c 执行完,现在如果 a 执行时间比 c 长,就变成了:
a(() => {
d();
});
看来只有完全隔离成两个函数:
(async () => {
await a();
b();
})();
(async () => {
await c();
d();
})();
或者利用 Promise.all:
async function ab() {
await a();
b();
}
async function cd() {
await c();
d();
}
Promise.all([ab(), cd()]);
参考文章:
1、精读《逃离 async/await 地狱》
2、ES6学习笔记——async 函数学习
3、MDN-Promise