---
# System prepended metadata

title: Async/Await
tags: [RD1]

---

# Async/Await
###### tags: `RD1`

:::spoiler 目錄
[TOC]
:::

## 什麼是 async？什麼是 await？

在 JavaScript 的世界，同步 sync 和非同步 async 的愛恨情仇，就如同偶像劇一般的剪不斷理還亂，特別像是setTimeout、setInterval、MLHttpRequest或fetch這些同步非同步混雜的用法，都會讓人一個頭兩個大，幸好 ES6 出現了 promise，ES7 出現了 async、await，幫助我們可以更容易的進行程式邏輯的撰寫。

對於同步和非同步，最常見的說法是「同步模式下，每個任務必須按照順序執行，後面的任務必須等待前面的任務執行完成，非同步模式則相反，後面的任務不用等前面的，各自執行各自的任務」，但我覺得這樣實在不容易理解，不容易理解的地方在於「中文」的同步和非同步，可能和實際上的解釋剛好相反了 ( 同步的中文字面意思是「一起走」，非同步的中文意思是「不要一起走」，超容易搞錯的 )，因此如果你跟我一樣也很容易搞錯，可以使用我覺得比較好理解的方法：「同一個步道 vs 不同步道」，透過步道的方式，就更容易明白同步和非同步。

> - 同步：在「同一個步道」比賽「接力賽跑」，當棒子沒有交給我，我就得等你，不能跑。
> - 非同步：在「不 ( 非 ) 同步道」比賽「賽跑」，誰都不等誰，只要輪到我跑，我就開始跑。

<img src="https://www.oxxostudio.tw/img/articles/201706/javascript-promise-settimeout-1.jpg">

在 ES7 裡頭 async 的本質是 promise 的語法糖 ( 包裝得甜甜的比較好吃下肚 )，只要 function 標記為 async，就表示裡頭可以撰寫 await 的同步語法，而 await 顧名思義就是「等待」，它會確保一個 promise 物件都解決 ( resolve ) 或出錯 ( reject ) 後才會進行下一步，當 async function 的內容全都結束後，會返回一個 promise，這表示後方可以使用.then語法來做連接，基本的程式長相就像下面這樣：

```javascript=
async function a(){
  await b();
  .....       // 等 b() 完成後才會執行
  await c();
  .....       // 等 c() 完成後才會執行
  await new Promise(resolve=>{
    .....
  });
  .....       // 上方的 promise 完成後才會執行
}
a();
a().then(()=>{
  .....       // 等 a() 完成後接著執行
});
```

## 利用 async 和 await 做個「漂亮的等待」

比較了解 async 和 await 的意思之後，就來試試看做個「漂亮的等待」，過去我在 JavaScript 同步延遲 ( Promise + setTimeout ) 一文裡，有使用 ES6 的 promise 來實現 delay ( 如同下方的程式範例 )，這個 delay 透過.then來完成一步一步的串接，雖然邏輯上很清楚，但若要實作比較複雜的流程，就得把每個程式寫在對應的 callback 裏，也就沒有想像的容易，這就是「不太漂亮的等待」 ( 使用 setTimeout 的做法就是不漂亮的等待 )。

```javascript=
const delay = (s) => {
  return new Promise(resolve => {
    setTimeout(resolve,s); 
  });
};

delay().then(() => {
  console.log(1);     // 顯示 1
  return delay(1000); // 延遲ㄧ秒
}).then(() => {
  console.log(2);     // 顯示 2
  return delay(2000); // 延遲二秒
}).then(() => {
  console.log(3);     // 顯示 3
});
```

如果我們把上面的範例修改為 async 和 await 的寫法，突然就發現程式碼看起來非常的乾淨，因為 await 會等待收到 resolve 之後才會進行後面的動作，如果沒有收到就會一直處在等待的狀態，所以什麼時候該等待，什麼時候該做下一步，就會非常清楚明瞭，這也就是我所謂「漂亮的等待」。

> 注意，await 一定得運行在 async function 內！

```javascript=
~async function{           // ~ 開頭表示直接執行這個 function，結尾有 ()
  const delay = (s) => {
    return new Promise(function(resolve){  // 回傳一個 promise
      setTimeout(resolve,s);               // 等待多少秒之後 resolve()
    });
  };

  console.log(1);      // 顯示 1
  await delay(1000);   // 延遲ㄧ秒
  console.log(2);      // 顯示 2
  await delay(2000);   // 延遲二秒
  console.log(3);      // 顯示 3
}();
```

## 搭配 Promise

基本上只要有 async 和 await 的地方，就一定有 promise 的存在，promise 顧名思義就是「保證執行之後才會做什麼事情」，剛剛使用了 async、await 和 promise 改善setTimeout這個容易出錯的非同步等待，針對setInterval，也能用同樣的做法修改，舉例來說，下面的程式碼執行之後，並「不會」如我們預期的「先顯示 1，再顯示 haha0...haha5，最後再顯示 2」，而是「先顯示 1 和 2，然後再出現 haha0...haha5」，因為雖然程式邏輯是從上往下，但在 count function 裏頭是非同步的語法，導致自己走自己的路，也造成了結果的不如預期。

```javascript=
const count = (t,s) => {
  let a = 0;
  let timer = setInterval(() => {
    console.log(`${t}${a}`);
    a = a + 1;
    if(a>5){
      clearInterval(timer);
    }
  },s);
};

console.log(1); 
count('haha', 100);
console.log(2);
```
<img src="https://www.oxxostudio.tw/img/articles/201908/js-async-await-01.jpg">

這時我們可以透過 async、await 和 promise 進行修正，在顯示 1 之後，會「等待」count function 結束後再顯示 2。

```javascript=
~async function(){  
  const count = (t,s) => {
      return new Promise(resolve => {
        let a = 0;
        let timer = setInterval(() => {
          console.log(`${t}${a}`);
          a = a + 1;
          if(a>5){
            clearInterval(timer);
            resolve();  // 表示完成
          }
        },s);
      });
    };

  console.log(1); 
  await count('haha', 100);
  console.log(2);
}();
```
<img src="https://www.oxxostudio.tw/img/articles/201908/js-async-await-02.jpg">

除了setTimeout和setInterval，這也可以用於像是「輸入文字」的情境，過去我們要做到「連續輸入」文字，可能要層層疊疊寫個好幾個 callback，現在如果使用 async 和 await，就能夠很簡單的實現連續輸入的情境，程式碼看起來也更乾淨簡潔。

```javascript=
// HTML 為一個輸入框、一個按鈕和一個 h1 標籤
// <input id="a"></input><button id="b">send</button>
// <h1 id="h"></h1>

~async function(){
  const input = () => {
    return new Promise(resolve =>{  
      const btnClick = () =>{
        h.insertAdjacentHTML('beforeend', a.value + '<br/>');   // 輸入後在 h1 裡添加內容
        a.value = '';   // 清空輸入框
        a.focus();      // 將焦點移至輸入框
        b.removeEventListener('click', btnClick);  // removeEventListener 避免重複綁定事件
        resolve();      // 完成
      };
      b.addEventListener('click', btnClick); // 綁定按鈕事件
    });
  };
  h.insertAdjacentHTML('beforeend', '開始<br/>');
  await input();     //  等待輸入，輸入後才會進行下一步
  await input();
  await input();
  h.insertAdjacentHTML('beforeend', '結束');
}();
```
<img src="https://www.oxxostudio.tw/img/articles/201908/js-async-await-03.gif">

## 搭配 Fetch

在我之前的文章JavaScript Fetch API 使用教學已經有提到fetch的用法，因為fetch最後回傳的是 promise，理所當然的透過 async 和 await 操作是最恰當不過的。

舉例來說，先前往 中央氣象局開放資料平台可以取得許多氣象資料，找到 局屬氣象站-現在天氣觀測報告，複製 JSON 格式的連結 ( 需要註冊登入才能看得到連結 )，透過fetch的json()方法處理檔案，目標顯示出「高雄市的即時氣溫」。

透過 async 和 await 的美化程式碼，得到的結果完全不需要 callback 的輔助，就能按照我們所期望的順序進行。( 先顯示「開始抓氣象」，接著顯示「高雄市的氣溫」，最後顯示「總算結束了」 )

```javascript=
~async function(){
    console.log('開始抓氣象');       // 先顯示「開始抓氣象」
    await fetch('氣象局 json 網址')  // 帶有 await 的 fetch
    .then(res => {
        return res.json();
    }).then(result => {
        let city = result.cwbopendata.location[14].parameter[0].parameterValue;
        let temp = result.cwbopendata.location[14].weatherElement[3].elementValue.value;
        console.log(`${city}的氣溫為 ${temp} 度 C`); 
    });
    console.log('總算結束了');       // 抓完氣象之後再顯示「總算結束了」
}();
```
<img src="https://www.oxxostudio.tw/img/articles/201908/js-async-await-04.jpg">

## 搭配 迴圈

如果要透過 JavaScript 實現「文字慢慢變大」的效果，除了透過 CSS 的 transition 設定之外，通常就是直接使用setInterval來完成，就像下面的程式碼這樣：

```javascript=
let size = 30;
h.style.lineHeight = 0;
const timer = setInterval(()=>{
  h.style.fontSize = size + 'px';
  size = size + 1;
  if(size >= 130){
    clearInterval(timer);
  }
},10);
```
如果搭配 async 和 await，我們就能將同樣的做法，改由「迴圈」實現，因為使用了 await，所以迴圈每次執行時，都會進行「等待」，也就能做到字體慢慢變大的效果。
```javascript=
// HTML 為一個 h1 標籤 <h1 id="h">hello</h1>

~async function(){
  const delay = t => {    // 先撰寫一個等待的 function
    return new Promise(resolve => {
      setTimeout(resolve, t);
    });
  };
  h.style.linHeight = 0;
  for(let i=30; i<130; i++){
    h.style.fontSize = i + 'px';
    await delay(10);    // 迴圈每次執行時，都會在這裡等待 10ms
  }
}();
```
<img src="https://www.oxxostudio.tw/img/articles/201908/js-async-await-05.gif">
同樣的，上面提到的 fetch 或是輸入文字，只要做成 await 的方式，都可以放在迴圈裡面使用，例如透過迴圈不斷的 fetch 資料、透過迴圈不斷的輸入文字...等，這些就不是 callback 方法能容易辦到的囉～

## 小結
坦白說只要你一但熟悉了 async 和 await，就真的回不去了，雖然說 callback 仍然是程式開發裡必備的功能，但對於同步和非同步之間的轉換，以後就交給 async 和 await 來處理吧！

## 參考資料

[簡單理解 JavaScript Async 和 Await](https://www.oxxostudio.tw/articles/201908/js-async-await.html)
[JavaScript Promise 全介紹](https://wcc723.github.io/development/2020/02/16/all-new-promise/)
[JavaScript 中的同步與非同步（上）：先成為 callback 大師吧！](https://blog.techbridge.cc/2019/10/05/javascript-async-sync-and-callback/)