async、await
August 16, 2022Promise
解決了非同步任務回呼地獄的問題,然而是屬於 API 層面的方案,基本上還是需要搭配回呼函式,而且本質上還是非同步的,總是沒那麼方便;ES8 以後有 async
、await
,從語法層面上支援非同步任務的程式碼撰寫,更易於與同步任務的程式碼結合運用。
async 函式
就語義上,async
用來標示函式執行時是非同步,也就是函式中定義了獨立於程式主流程的任務,若呼叫 async
函式的話,會傳回一個 Promise
物件:
> async function foo() {
... return 'foo';
... }
undefined
> foo()
Promise { 'foo' }
> foo().then(console.log)
Promise { <pending> }
> foo
[AsyncFunction: foo]
>
async
函式是 AsyncFunction
的實例,型態為 Function
的子類型,雖然函式中定義了 return
傳回字串 'foo'
,實際上這代表了 async
函式傳回的 Promise
達成任務後的結果,若要取得結果,方式之一就是使用 Promise
的 then
方法,另一個方式是使用稍後會介紹的 await
來取得。
async
函式中 return
的結果,若是 Promise
或 thenable 物件,可以取得該物件達成任務後的結果,因此可用來銜接其他傳回 Promise
的非同步函式。例如,底下範例會在 1 秒之後顯示數字的結果(而不是顯示 Promise
實例):
function asyncFoo(n) {
return new Promise(resolve => {
setTimeout(
() => resolve(n * Math.random()),
1000
);
});
}
async function foo(n) {
return asyncFoo(n);
}
foo(10).then(v => console.log(v));
如果 async
函式執行之後,無法達成任務呢?可以使用 throw
,例如:
async function randomDivided(divisor) {
let n = Math.floor(Math.random() * 10);
if(n !== 0) {
return divisor / n;
}
throw new Error('Shit happens: divided by zero');
}
randomDivided(10)
.then(
n => console.log(n),
err => console.log(err)
);
在 async
函式中若 throw
,表示任務無法達成,也就可以使用 Promise
的 then
的第二個回呼函式來處理。
await 與 Promise
ES8 以後與 async
搭配的是 await
,就語義上,若想等待 async
函式執行完後,再執行後續的流程,可以使用 await
。例如:
async function foo() {
return 'foo';
}
async function main() {
let r = await foo();
console.log(r); // 顯示 foo
}
main();
await
必須撰寫在 async
函式之中,若不想撰寫 main
函式,另一個方案是使用 IIFE:
async function foo() {
return 'foo';
}
(async function() {
let r = await foo();
console.log(r);
})();
ES13 以後,可以在頂層直接撰寫 await
,如果你的環境已經實現該特性,就可以這麼寫:
async function foo() {
return 'foo';
}
let r = await foo();
console.log(r);
實際上,await
可以接上任何值,不一定要搭配 async
函式,只不過若是接上 Promise
實例,會在任務達成時,取得達成值作為 await
的結果:
function asyncFoo(n) {
return new Promise(resolve => {
setTimeout(
() => resolve(n * Math.random()),
2000
);
});
}
(async function() {
let r1 = await asyncFoo(10);
let r2 = await asyncFoo(r1);
let r3 = await asyncFoo(r2);
console.log(r3);
})();
await
時 Promise
若還沒達成任務,只是不會執行 async
函式中定義的後續流程,然而底層的事件迴圈並沒有被阻斷,若檢查到事件佇列中有新事件,還是會執行對應的任務;例如若在上例中加段程式碼:
…
setTimeout(
() => console.log(n * Math.random()),
1000
);
(async function() {
let r1 = await asyncFoo(10);
let r2 = await asyncFoo(r1);
let r3 = await asyncFoo(r2);
console.log(r3);
})();
在程式碼執行 await asyncFoo(10)
後,需要 2 秒的時間才能取得任務達成值,然而 1 秒時間到時,setTimeout
設定的回呼函式還是會執行。
有些開發者會熟悉支援執行緒的語言,在 JavaScript 中會想尋找 sleep
之類、令執行緒停止指定時間的功能;然而,JavaScript 不支援多執行緒,若令唯一的執行緒停止指定時間,結果會像在櫃台前排隊時,令櫃台人員發呆指定的時間,什麼事都不做,想想看隊伍後頭的人會怎樣?
然而基於方才的描述,await
時 Promise
若還沒達成任務,底層的事件迴圈並沒有被阻斷,因而透過 await
與 Promise
,倒是可以模擬 sleep
的功能,例如底下的程式片段,就流程來說確實是間隔了一秒:
async function sleep(millis) {
return new Promise(resolve => setTimeout(resolve, millis));
}
(async function() {
console.log(new Date());
await sleep(1000);
console.log(new Date());
})();
並不是有了 async
、await
,就不需要 Promise
了,理由之一是 async
函式執行後也是傳回 Promise
;另一個理由是在某些場合,必須使用 Promise
來銜接回呼風格的 API。例如,Node.js 的 fs
模組中 readFile
函式,就可以自訂個 readTxtFile
如下傳回 Promise
:
let fs = require('fs');
function readTxtFile(filename) {
return new Promise(resolve => {
fs.readFile(filename, function(_, content) {
resolve(content.toString());
});
});
}
readTxtFile('foo.js')
.then(console.log);
使用 Promise
來銜接回呼風格的API之後,進一步地就可以搭配 async
、await
來使用了。例如:
(async function() {
let content = await readTxtFile('foo.js');
console.log(content);
})();