Day 16 - Asynchronous 非同步进化顺序 - Async/Await

前言

昨天聊了 callback 与 Promise,是如何过关斩将,不断克服障碍走到 ES6。

然而只要程序规模不断扩张,就永远会有更高阶的需求产生,让我们继续来看,Promise 之後发生了什麽故事吧!

非同步第三步 - async await

ES6 Promise 已经是满不错的设计,只是它长得还是太像非同步了(?),如果我有资料仰赖非同步取回,就会变成同步跟非同步两个大区块,但 ES7 新登场的 async/await,开始渐渐模糊了同步与非同步的界线。

async/await 是 ES7 版本的一个 Promise 语法糖,透过 asyncawait 两个关键字,可以将原本执行多行 Promise 程序简化成一行,并且使用方式非常贴近一般的同步程序码,大幅提高程序的可读性。

  • async 关键字放在 function 的前面,代表「宣告一个非同步的函式
  • await 关键字放在呼叫 async function 的前面,代表「呼叫并等待这个非同步函式
  • async function 必须回传 Promise 物件
  • asyncawait 是成对出现的

async function 其实也是一种物件种类,有兴趣可以查看 Mozilla MDN

await 的两种摆法

// 用 setTimeout 模拟一个要等 3 秒的 Promise
const wait3Seconds = async (x) => {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 3000);
  });
}

// 函式先 await 再赋值
const a = await wait3Seconds(1);
const b = await wait3Seconds(2);
const sum = a + b;
console.log(sum); // 3

// 函式先赋值再 await
const c = wait3Seconds(1);
const d = wait3Seconds(2);
const sum2 = await c + await d;
console.log(sum2); // 3

有注意到上面的例子中,await 的摆放位置不同吗?可是执行出来的 sum 跟 sum2 结果都一样耶,所以 await 放哪很重要吗?

好奇的话不妨复制贴到 console 跑跑看,记得分两段跑,1~14 行先跑,看完结果再跑 17~20 行。

先公布答案:

第一段要等 6 秒才会有结果,而第二段只要等 3 秒。为什麽呢?

新手如果不熟,不妨这样记忆:看到 await 代表要等

第一段 (函式先 await 再赋值):

  1. 程序执行到第 11 行,看到 await 所以要等,setTimeout 被启动开始倒数
  2. (等了 3 秒之後) 赋值给 a
  3. 程序执行到第 12 行,看到 await 所以要等,setTimeout 被启动开始倒数
  4. (等了 3 秒之後) 赋值给 b
  5. 程序执行到第 13、14 行,相加之後印出

第二段 (函式先赋值再 await):

  1. 程序执行到第 17 行,没有 await 所以不等,把 Promise 物件赋值给 csetTimeout 被启动开始倒数
  2. 程序执行到第 18 行,没有 await 所以不等,把 Promise 物件赋值给 dsetTimeout 被启动开始倒数
  3. 程序执行到第 19 行,看到 await 所以要等 c 完成
  4. (等了 3 秒之後)
  5. 看到 await 所以要等 d 完成,但刚刚那 3 秒已经让 d 也完成了,所以相加之後印出

另一个值得思考的点是,因为 await 的顺序不同,导致 ab 这两个变数存的是 number,而 cd 则存了 Promise 物件,有兴趣不妨印出来看看。

async await 解决了什麽问题

简化了 Promise 复杂的结构,让非同步函式可以变得像是同步一样,不仅可读性更高,在 error handling 的方面,也可以使用既有的 try catch 来解决,某种程度上也让新手更容易上手。

async await 实战

如同昨天 Promise 的例子,同步/非同步的界线非常鲜明,要改动常常要顾虑这一行到底是同步还非同步,但改用 async/await 之後,看起来全都像是同步程序码:

const doQueryWallet = async () => {
    // 这里都跟原本一样,只是多了 async
};

const doOrder = async () => {
    // 这里都跟原本一样,只是多了 async
};

const wallet = await doQueryWallet();
if (wallet.balance > 0) {
    const order = await doOrder();
    if (order.status === 'ok') {
        console.log('下单成功');
    } else {
        console.log('下单失败');
    }
} else {
    console.log('余额不足');
}

当然 error handling 的部分,也可以用熟悉的 try catch 包起来:

try {
    const wallet = await doQueryWallet();
    if (wallet.balance > 0) {
        const order = await doOrder();
        if (order.status === 'ok') {
            console.log('下单成功');
        } else {
            console.log('下单失败');
        }
    } else {
        console.log('余额不足');
    }
} catch (err) {
    console.error(err);
}

非同步常见的三种顺序

以上讲的 case,大部分都还是基於「由上而下」的顺序,也就是我们很熟悉同步的程序码顺序,一个一个由上而下执行,第二行绝对会等第一行结束才执行。

非同步有以下三种常见的顺序,可以透过 async/await 及 Promise 简单达到以下效果:

  • sequence (序列)
  • parallel (平行)
  • race (竞争)

sequence

这是我们最常见的顺序,A 执行完换 B,B执行完换 C,後者不能早於前者,通常是因为後者依赖於前者的资料

比如:一定要先查完帐户余额,确定有余额才能够下单。

const sequence = async () => {
    const output1 = await a();
    const output2 = await b();
    const output3 = await c();
    return `sequence is done ${output1} ${output2} ${output3}`;
}
成功的情况

a
|--------|
         b
         |--------|
                  c
                  |--------|
                           非同步结束
                           |----------
拒绝的情况

a
|--------|
         b
         |--------X
                  非同步结束(throw error)
                  |--------

parallel

这是一个比较有效率的执行方式,不管顺序,把所有非同步程序都撒出去执行,等全部都完成(fulfilled)再一次告诉我。通常代表这几个非同步函式没有互相依赖的资料,可以同时执行

比如:查询店家评论、查询我的优惠券、查询帐户余额,三者没有互相依赖的资料,但是会出现在同一个页面,因此把三个函式都发出去,全部回来再一次做接下来的动作(比如把 loading 图示关掉)。

const parallel = async () => {
    const [output1, output2, output3] = await Promise.all([a(), b(), c()]);
    return `parallel is done ${output1} ${output2} ${output3}`;
}
成功的情况

a
|-----|
b
|--------------|
c
|--------|
               非同步结束,取得回传结果
               |----------
拒绝的情况

a
|-----X
b
|--------------|
c
|--------|
      非同步结束,取得回传错误
      |----------

注意,成功与失败的回传值是不同的,成功时把结果包成一个 array 回传,里头的顺序即 Promise.all 里面的顺序。而拒绝则是任何一个 Promise 拒绝(reject)就会立刻回传最早拒绝的结果。

注意,Promise 是不可打断的,发出去的函式跟泼出去的水一样,唯一能做的,就是忽略回传值,所以即便上图 a 最早就被拒绝了,b 跟 c 还是会把他们该做的事情在背景跑完,只是我们不会收到结果就是了。

race

这算是 parallel 的一个变种(?),也是把所有非同步程序都撒出去执行,但就像赛跑一样,只要任何一个完成(fulfilled)或拒绝(reject),就立刻回传告诉我,其他未完成的则直接忽略,比较是运用在较特殊的情境。

比如:可以帮 request 设定一个 timeout 的时间限制,放一个 setTimout 10 秒就会 reject 的 Promise 进去 Promise.race,就会强制在 10 秒之内得到结果。

成功的情况

a
|-----|
b
|--------------|
c
|--------|
      非同步结束,取得回传结果
      |----------
拒绝的情况

a
|-----X
b
|--------------|
c
|--------|
      非同步结束,取得回传错误
      |----------
const race = async () => {
    const output1 = await Promise.race([a(), b(), c()]);
    return `race is done ${output1}`;
}

结语

非同步很复杂,但也就是因为很复杂,可以玩的花样更多,许多复杂的需求其实都是建构在非同步程序中,否则我们的程序就永远只能同步一行一行执行,似乎也少了一些醍醐味。

无论如何看待
我终将完成我的
承诺

参考资料

JAVACRIPT.INFO: Promises, async/await
MDN - Promise.all
MDN - Promise.race


<<:  【第16天】训练模型-DenseNet201

>>:  Eloquent ORM - 一对一关联

[NestJS 带你飞!] DAY17 - Injection Scopes

Nest 在大多数情况下是采用 单例模式 (Singleton pattern) 来维护各个实例,也...

IOS、Python自学心得30天 Day-13 模组训练改善-5

前言: 藉由训练过程中的检查点纪录,可以知道此模型的训练次数,不过若不是特别需要,平常可以注解掉,让...

laravel 8 (一) 建立专案及资料库设定

安装专案 composer global require laravel/installer //将...

[Day 27] Web 小迷茫

今天要解的题目和昨天有关联 是他进阶的2.0 没错,又是爱尔兰人 Irish-Name-Repo 2...

Day 12 [Python ML、特徵工程] 特徵工程整理

Categorical Encoding Encoding Describe One hot enc...