本文參考網址
什麼是 async
?什麼是 await
?
在 JavaScript 的世界,同步 sync 和非同步 async 的愛恨情仇,就如同偶像劇一般的剪不斷理還亂,特別像是setTimeout、setInterval、MLHttpRequest或fetch這些同步非同步混雜的用法,都會讓人一個頭兩個大,幸好 ES6 出現了 promise,ES7 出現了 async、await,幫助我們可以更容易的進行程式邏輯的撰寫。
對於同步和非同步,最常見的說法是「同步模式下,每個任務必須按照順序執行,後面的任務必須等待前面的任務執行完成,非同步模式則相反,後面的任務不用等前面的,各自執行各自的任務」,但我覺得這樣實在不容易理解,不容易理解的地方在於「中文」的同步和非同步,可能和實際上的解釋剛好相反了 ( 同步的中文字面意思是「一起走」,非同步的中文意思是「不要一起走」,超容易搞錯的 ),因此如果你跟我一樣也很容易搞錯,可以使用我覺得比較好理解的方法:「同一個步道 vs 不同步道」,透過步道的方式,就更容易明白同步和非同步。
同步:在「同一個步道」比賽「接力賽跑」,當棒子沒有交給我,我就得等你,不能跑。
非同步:在「不 ( 非 ) 同步道」比賽「賽跑」,誰都不等誰,只要輪到我跑,我就開始跑。
在 ES7 裡頭 async 的本質是 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 和 await 做個「漂亮的等待」
比較了解 async 和 await 的意思之後,就來試試看做個「漂亮的等待」,過去有使用 ES6 的 promise 來實現 delay ( 如同下方的程式範例 ),這個 delay 透過.then來完成一步一步的串接,雖然邏輯上很清楚,但若要實作比較複雜的流程,就得把每個程式寫在對應的 callback 裏,也就沒有想像的容易,這就是「不太漂亮的等待」 ( 使用 setTimeout 的做法就是不漂亮的等待 )。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // 不漂亮的等待
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 內!
1
2
3
4
5
6
7
8
9
10
11
12
13
| ~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 裏頭是非同步的語法,導致自己走自己的路,也造成了結果的不如預期。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| 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);
|
這時我們可以透過 async、await 和 promise 進行修正,在顯示 1 之後,會「等待」count function 結束後再顯示 2。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| ~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);
}();
|
除了setTimeout和setInterval,這也可以用於像是「輸入文字」的情境,過去我們要做到「連續輸入」文字,可能要層層疊疊寫個好幾個 callback,現在如果使用 async 和 await,就能夠很簡單的實現連續輸入的情境,程式碼看起來也更乾淨簡潔。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // 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', '結束');
}();
|
搭配 Fetch
在我之前的文章 JavaScript Fetch API 使用教學 已經有提到fetch的用法,因為fetch最後回傳的是 promise,理所當然的透過 async 和 await 操作是最恰當不過的。
舉例來說,先前往 中央氣象局開放資料平台 可以取得許多氣象資料,找到 局屬氣象站-現在天氣觀測報告 ,複製 JSON 格式的連結 ( 需要註冊登入才能看得到連結 ),透過fetch的json()方法處理檔案,目標顯示出「高雄市的即時氣溫」。
透過 async 和 await 的美化程式碼,得到的結果完全不需要 callback 的輔助,就能按照我們所期望的順序進行。(先顯示「開始抓氣象」,接著顯示「高雄市的氣溫」,最後顯示「總算結束了」)
1
2
3
4
5
6
7
8
9
10
11
12
| ~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('總算結束了'); // 抓完氣象之後再顯示「總算結束了」
}();
|
搭配 迴圈
如果要透過 JavaScript 實現「文字慢慢變大」的效果,除了透過 CSS 的 transition 設定之外,通常就是直接使用setInterval來完成,就像下面的程式碼這樣:
1
2
3
4
5
6
7
8
9
| 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,所以迴圈每次執行時,都會進行「等待」,也就能做到字體慢慢變大的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 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
}
}();
|
同樣的,上面提到的 fetch 或是輸入文字,只要做成 await 的方式,都可以放在迴圈裡面使用,例如透過迴圈不斷的 fetch 資料、透過迴圈不斷的輸入文字…等,這些就不是 callback 方法能容易辦到的囉
小結
坦白說只要你一但熟悉了 async 和 await,就真的回不去了,雖然說 callback 仍然是程式開發裡必備的功能,但對於同步和非同步之間的轉換,以後就交給 async 和 await 來處理吧!