从 Promise到async/await,方便了我们对异步的控制,可以使用写同步代码的方式写异步代码,但同时一不小心也会产生一些错误。
常见错误:
1、返回Promise的函数(return Promise的函数,或者async定义的函数)没有加await使用。
2、没有处理async函数里的异常。
3、本来可以异步并发请求的函数,通过滥用await写成了串行同步,损失了性能。
在展开讲解这些错误之前,我们先来看看async / await为什么会存在。
async关键字可以用来定义一个async函数,这个函数有两个功能:
因为async函数一定会返回一个Promise,如果返回值不是Promise,它就会被包装在一个Promise中。
例如,如下代码:
async function foo() {
return 1;
}
等价于:
function foo() {
return Promise.resolve(1);
}
await关键字是一个操作符,用来等待await后面的表达式(可以是Promise实例,或者任意类型的值)执行完成以后再执行下一条语句。
比如,如下代码:
function resolveAfter2Seconds(x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function f1() {
let x = await resolveAfter2Seconds(10);
console.log(x); // 10
}
f1();
resolveAfter2Seconds函数返回的是一个Promise,使用await关键字后,并不需要使用.then语句就拿到函数返回的Promise最终值是10。
async和 await 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 Promise。
看完介绍感觉棒棒哒,我们接着来看问题。
比如,如下代码
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling');
const result = resolveAfter2Seconds();
console.log(result);
// result的值会是Promise { }
}
asyncCall();
这段代码只比上一段代码少了一个await,就导致result的值变成了Promise,而不是一个已经完成状态的Promise,或者是resolve以后的值。
这是因为await关键字会等待后面的表达式返回的Promise执行完毕才会执行下面的语句,如果没有使用await,resolveAfter2Seconds函数返回的是一个Promise,要等待2秒这个Promise才会执行完毕,而result的值将会一直是这个Promise,如果我们要获得这个Promise返回的值'resolved'。
我们可以有两种方法:
console.log(await result); // "resolved"
result.then((value) => console.log(value)); // "resolved"
我们来看async函数的情况
比如,如下代码:
async function foo() {
return 1;
}
async function asyncCall() {
const result = foo();
console.log(result); // Promise { 1 }
console.log(await result); // 1
result.then((value) => console.log(value)); // 1
}
asyncCall();
同样的结果,虽然foo函数的返回值是1,但是因为加了async关键字,导致foo函数的返回结果被包装成了一个完成状态的Promise。
注意:如果在编码过程中,调用async函数后,期望获得真正的值,但是又没有加await关键字,就会导致获取的值不正确。
有一个学长曾经说过,编程就是捕获异常,处理异常。我不是很理解,除了异常,需求去那里了呢?
我们来看看下面这段代码:
async function foo() {
throw Error('some foo error');
}
async function asyncCall() {
const result = await foo();
}
asyncCall();
运行以后会报一个错误:UnhandledPromiseRejectionWarning: Error: some foo error
意思是出现了一个未处理的Promise错误。
如果我们把上面的代码改成加了try/catch会怎么样呢?
比如,如下代码:
async function foo() {
throw Error('some foo error');
}
async function asyncCall() {
try {
const result = await foo();
console.log('result:', result);
} catch (e) {
console.log('catch:', e.message); // catch: some foo error
}
}
asyncCall();
加了try/catch以后,catch里成功捕获了foo函数抛出的异常。
但是try/catch会有一种情况捕获不了async函数里的异常,请看下面代码:
async function asyncCall() {
try {
return Promise.reject(new Error('Oops!'));
console.log('result:', result);
} catch (e) {
console.log('catch:', e.message);
}
}
asyncCall();
try语句里直接return一个Promise.reject,是没法捕获的,不过如果我们在Promise.reject前面加一个await关键字,就能捕获了。
除了这样,其实还有其他办法,因为async函数返回的是一个Promise,所以我们可以在执行asyncCall函数的时候,加一个catch在后面,就像这样:
async function asyncCall() {
try {
return Promise.reject(new Error('Oops!'));
console.log('result:', result);
} catch (e) {
console.log('catch:', e.message);
}
}
function handleError (err) {
console.log('handleError:', err.message);
}
asyncCall().catch(handleError);
这样就能成功捕获到return语句触发的异常。
不过存在这样一种情况,就是在async函数里,调用其他async函数,这些其他的async函数如果没有在return语句里,前面也没有使用await关键字,后面也没有使用.catch捕获异常。
那么这些async函数内部的异常将不会被捕获,这是一个使用async函数的坑。
注意:因为async/await实际上只是简化Promise的写法,Promise本身存在的问题和限制自然也得到了保留,写Promise不用catch捕获不了异常,异步函数不用await等待执行完成,try/catch也捕获不了异常。
我们先来看一个例子,有点长,得仔细慢慢看。
function resolveAfter(seconds, msg) {
console.log("starting resolveAfter");
return new Promise((resolve) => {
setTimeout(() => {
resolve(msg);
}, seconds);
});
}
async function sequentialStart() {
console.log("==SEQUENTIAL START==");
// 1. Execution gets here almost instantly
const slow = await resolveAfter(2000, 'slow');
console.log(slow); // 2. this runs 2 seconds after 1.
const fast = await resolveAfter(1000, 'fast');
console.log(fast); // 3. this runs 3 seconds after 1.
}
async function concurrentStart() {
console.log("==CONCURRENT START with await==");
const slow = resolveAfter(2000, 'slow'); // starts timer immediately
const fast = resolveAfter(1000, 'fast'); // starts timer immediately
// 1. Execution gets here almost instantly
console.log(await slow); // 2. this runs 2 seconds after 1.
console.log(await fast); // 3. this runs 2 seconds after 1., immediately after 2., since fast is already resolved
}
function concurrentPromise() {
console.log("==CONCURRENT START with Promise.all==");
return Promise.all([resolveAfter(2000, 'slow'), resolveAfter(1000, 'fast')]).then(
(messages) => {
console.log(messages[0]); // slow
console.log(messages[1]); // fast
}
);
}
async function parallel() {
console.log("==PARALLEL with await Promise.all==");
// Start 2 "jobs" in parallel and wait for both of them to complete
await Promise.all([
(async () => console.log(await resolveAfter(2000, 'slow')))(),
(async () => console.log(await resolveAfter(1000, 'fast')))(),
]);
}
// 串行执行,总耗时3秒
sequentialStart();
// 并发执行,总耗时2秒,但是会等待slow执行完成以后才会输出fast的结果
setTimeout(concurrentStart, 4000);
// 同样并发执行,总耗时2秒,会等待Promise.all里所有的Promise都执行完毕才会进行输出,耗时2秒
setTimeout(concurrentPromise, 7000);
// 并发执行,总耗时2秒,但是因为在Promise.all里的Promise里直接打印了结果,
// 1秒以后,fast会先打印,2秒以后slow会进行打印
setTimeout(parallel, 10000);
代码里写了4个函数,都在调用resolveAfter函数,一个延迟2秒执行,一个延迟1秒执行。
由于使用了不同的方式执行2个不同的resolveAfter函数,导致总执行时间会有差异。
如果把resolveAfter换成现实业务里需要请求多个独立的接口。
那么使用sequentialStart函数就会导致耗时过久。
使用concurrentStart和concurrentPromise函数总耗时会是最慢的那个函数。
使用parallel(JS运行时里没有并行,只能并发,不要被函数名字误导)函数,总耗时同样会是最慢的那个函数,但是可以在一个请求返回以后马上就进行处理,而不同等待所有函数返回再处理。
1、如果需要串行化处理异步函数,使用async/await将是非常好的选择,但是别忘记处理异常。
2、如果要并发处理异步函数,得使用Promise.all。
3、有人建议避免使用async/await,因为完全可以使用Promise进行替代,而且async/await会加重心智负担,因为你得先完全理解Promise才能用搞懂async/await在做什么,我觉得在把异步函数当同步代码写的场景里, async/await还是大有用处。
整篇文章看完,如果用一句话总结:
使用async/await最重要的事情,就是async函数会返回一个Promise,await关键字能等待async函数的Promise执行完毕,且能把异常交给try/catch语句处理。
http://thecodebarbarian.com/async-await-error-handling-in-javascript.html
async function - JavaScript | MDN
https://uniqname.medium.com/why-i-avoid-async-await-7be98014b73e
Avoid async/await hell - DEV Community