理解 JavaScript 中的 Async/Await

Posted by Peter Dong on October 22, 2022

async/await 是以更舒适的方式使用 promise 的一种特殊语法。

在 Salesforce 中,如果你有开发过 lwc 相关的组件,想必应该使用过 Async/Await 语法。其实在 JavaScript 中做异步开发时,我们通常会毫不犹豫的使用 Async/Await. 不管是并发还是串行,Async/Await 都能处理的很好,而且还保证了代码的可读性。本篇内容主要是我根据一些资料和官方文档来阐述对 Async/Await 语法的一些理解,如有叙述不对的地方,欢迎指正。

什么是 async/await?

JavaScript 中的 async/awaitAsyncFunction 特性中的关键字。目前为止,除了 IE 之外,常用浏览器和 Node.js (v7.6+) 都已经支持该特性。具体支持情况可以在参考 这里。

async/await 是一种建立在 Promise 之上的编写异步或非阻塞代码的新方法 (Generator 的语法糖), 普遍认为是 JS 异步操作的最优雅的解决方案。相对于 Promise 和回调,它的可读性和简洁度都更高。只要 function 标记为 async, 就表示函数里面可以写 await 的同步语法,而 await 顾名思义就是「等待」,它会确保一个 promise 都解決 ( resolve ) 或出错 ( reject ) 后才会进行下一步,当 async function 的内容全部执行结束,会返回一个 promise, 表示后续代码可以使用 .then 语法來连接,基本的代码就像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function a(){
  await b();
  .....       // 等 b() 完成后才会执行
  await c();
  .....       // 等 c() 完成后才会执行
  await new Promise(resolve=>{
    .....
  });
  .....       // 上方的 promise 完成后才会执行
}

a();
a().then(()=>{
  .....       // 等 a() 完成后接著执行
});

async 的作用?

我们先看如下代码的输出结果是什么:

1
2
3
4
5
6
async function testAsync() {
    return "hello async";
}

const result = testAsync();
console.log(result); //Promise {<fulfilled>: 'hello async'}

所以,async 函数返回的是其实一个 Promise 对象。从文档中也可以得到这个信息。async 函数会返回一个 Promise 对象。如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不用 await 获取其返回值的情况下,我们可以用原来的方式:then() 链来处理这个 Promise 对象,就像这样:

1
2
3
testAsync().then(v => {
    console.log(v);    // 输出 hello async
});

await 在等待什么呢?

根据 MDN 文档 上写的:

1
[return_value] = await expression;

await 等待的是一个表达式,那么表达式,可以是一个常量 ,变量,promise, 函数等,换句话说,就是没有特殊限定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

test(); // something hello async

为什么 await 关键词只能在 async 函数中用?

await 操作符等的是一个返回的结果,如果它等到的是一个 Promise 对象,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

async/await 的优势

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们先用 setTimeout 来模拟异步操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

//step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1512.908935546875 ms

如果用 async/await 来实现呢,会是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1512.2080078125 ms

结果和之前的 Promise 实现是一样的,但是这个代码看起来清晰得多,几乎跟同步代码一样。

思考:为什么需要异步编程?

JavaScript 是单线程的,就是必须等待上一个任务执行完才能执行下一个任务,这种执行模式叫:同步.

但是随着业务复杂度的提升,很多情况下有处理高并发 (单位时间内极大的访问量) 和 I/O 密集场景 (ps: I/O 操作往往非常耗时,所以异步的关键在于解决 I/O 耗时问题), 如果采用同步编程,问题就来了,服务器处理一个 I/O 请求需要大量的时间,后面的请求都将排队,造成浏览器端的卡顿。异步编程能解决这个问题。


Buy Me a Coffee