async / await 使用过程中容易出错的地方

背景

从 Promise到async/await,方便了我们对异步的控制,可以使用写同步代码的方式写异步代码,但同时一不小心也会产生一些错误。

常见错误:

1、返回Promise的函数(return Promise的函数,或者async定义的函数)没有加await使用。

2、没有处理async函数里的异常。

3、本来可以异步并发请求的函数,通过滥用await写成了串行同步,损失了性能。

async / await 定义

在展开讲解这些错误之前,我们先来看看async / await为什么会存在。

async关键字可以用来定义一个async函数,这个函数有两个功能:

1、把函数返回值包装为Promise。

因为async函数一定会返回一个Promise,如果返回值不是Promise,它就会被包装在一个Promise中。

例如,如下代码:

async function foo() {
  return 1;
}

等价于:

function foo() {
  return Promise.resolve(1);
}

2、可以在async函数里使用await关键字。

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。

asyncawait 关键字让我们可以用一种更简洁的方式写出基于 Promise 的异步行为,而无需刻意地链式调用 Promise。

看完介绍感觉棒棒哒,我们接着来看问题。

常见错误

1、返回Promise的函数(return Promise的函数,或者async定义的函数)没有加await使用。

比如,如下代码

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关键字,就会导致获取的值不正确。

2、没有处理async函数里的异常。

有一个学长曾经说过,编程就是捕获异常,处理异常。我不是很理解,除了异常,需求去那里了呢?

我们来看看下面这段代码:

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也捕获不了异常。

3、本来可以异步并发处理的函数,通过滥用await写成了串行同步,损失了性能。

我们先来看一个例子,有点长,得仔细慢慢看。

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 ‍‍

你可能感兴趣的:(开发问题汇总,javascript,nodejs,前端)