非同步函式可讓您編寫以承諾為基礎的程式碼,就像是同步程式碼一樣。
根據預設,Chrome、Edge、Firefox 和 Safari 都會啟用非同步函式,而且這些函式相當出色。您可以使用這些方法,以同步方式編寫以承諾為基礎的程式碼,但不會封鎖主執行緒。這類程式碼會讓非同步程式碼變得「不夠聰明」,但更易於閱讀。
非同步函式的運作方式如下:
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
} catch (rejectedValue) {
// …
}
}
如果您在函式定義前使用 async
關鍵字,就可以在函式中使用 await
。await
承諾時,函式會以非阻塞的方式暫停,直到承諾解決為止。如果應許已兌現,您就會收到該值。如果應許遭到拒絕,系統會擲回遭拒絕的值。
瀏覽器支援
範例:記錄擷取作業
假設您想擷取網址,並將回應記錄為文字。以下是使用承諾的情況:
function logFetch(url) {
return fetch(url)
.then((response) => response.text())
.then((text) => {
console.log(text);
})
.catch((err) => {
console.error('fetch failed', err);
});
}
以下是使用非同步函式的相同內容:
async function logFetch(url) {
try {
const response = await fetch(url);
console.log(await response.text());
} catch (err) {
console.log('fetch failed', err);
}
}
它有相同的列數,但所有回呼都消失了。這樣一來,讀者就更容易閱讀,尤其是不熟悉承諾的讀者。
非同步回傳值
無論是否使用 await
,非同步函式都會一律傳回承諾。該承諾會根據非同步函式傳回的內容解析,或根據非同步函式擲回的內容拒絕。因此,如果使用:
// wait ms milliseconds
function wait(ms) {
return new Promise((r) => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}
…呼叫 hello()
會傳回承諾,並透過 "world"
履行。
async function foo() {
await wait(500);
throw Error('bar');
}
…呼叫 foo()
會傳回一個承諾,該承諾會使用 Error('bar')
拒絕。
範例:串流回應
在更複雜的範例中,非同步函式的優點會更加明顯。假設您想在記錄區塊時串流回應,並傳回最終大小。
以下是使用承諾的範例:
function getResponseSize(url) {
return fetch(url).then((response) => {
const reader = response.body.getReader();
let total = 0;
return reader.read().then(function processResult(result) {
if (result.done) return total;
const value = result.value;
total += value.length;
console.log('Received chunk', value);
return reader.read().then(processResult);
});
});
}
看看我這位「承諾使用者」Jake Archibald。請注意,我如何在 processResult()
內呼叫自身,以設定非同步迴圈?寫出讓我覺得很聰明的內容。但就像大多數「聰明」的程式碼一樣,您必須盯著程式碼看很久,才能瞭解程式碼的運作方式,就像 90 年代的魔幻立體圖片一樣。
請再試一次,使用非同步函式:
async function getResponseSize(url) {
const response = await fetch(url);
const reader = response.body.getReader();
let result = await reader.read();
let total = 0;
while (!result.done) {
const value = result.value;
total += value.length;
console.log('Received chunk', value);
// get the next result
result = await reader.read();
}
return total;
}
所有「智慧」功能都消失了。讓我覺得很自豪的非同步迴圈,已被可靠但乏味的 while 迴圈取代。這樣好多了。日後您將獲得非同步疊代器,可以 for-of 迴圈取代 while
迴圈,讓程式碼更整齊。
其他非同步函式語法
我已經向您展示 async function() {}
,但 async
關鍵字可搭配其他函式語法使用:
箭頭函式
// map some URLs to json-promises
const jsonPromises = urls.map(async (url) => {
const response = await fetch(url);
return response.json();
});
物件方法
const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(`/avatars/${name}.jpg`);
}
};
storage.getAvatar('jaffathecake').then(…);
類別方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jaffathecake').then(…);
請留意!避免過於依序
雖然您編寫的程式碼看起來是同步的,但請務必把握機會並行執行。
async function series() {
await wait(500); // Wait 500ms…
await wait(500); // …then wait another 500ms.
return 'done!';
}
上述作業需要 1000 毫秒才能完成,而:
async function parallel() {
const wait1 = wait(500); // Start a 500ms timer asynchronously…
const wait2 = wait(500); // …meaning this timer happens in parallel.
await Promise.all([wait1, wait2]); // Wait for both timers in parallel.
return 'done!';
}
上述作業需要 500 毫秒才能完成,因為兩個等待作業會同時發生。我們來看看實際範例。
範例:依序輸出擷取內容
假設您想擷取一系列網址,並盡快以正確順序記錄這些網址。
深呼吸 - 以下是使用承諾的情況:
function markHandled(promise) {
promise.catch(() => {});
return promise;
}
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map((url) => {
return markHandled(fetch(url).then((response) => response.text()));
});
// log them in order
return textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise).then((text) => console.log(text));
}, Promise.resolve());
}
沒錯,我使用 reduce
連結一系列承諾。我很聰明。不過,這麼做有點太聰明,建議您不要這麼做。
不過,將上述內容轉換為非同步函式時,很容易會變得過於順序:
async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } }
function markHandled(...promises) { Promise.allSettled(promises); } async function logInOrder(urls) { // fetch all the URLs in parallel const textPromises = urls.map(async (url) => { const response = await fetch(url); return response.text(); }); markHandled(...textPromises); // log them in sequence for (const textPromise of textPromises) { console.log(await textPromise); } }
瀏覽器支援因應措施:產生器
如果您指定支援產生器的瀏覽器 (包括各大瀏覽器的最新版本),就可以使用類似 polyfill 的非同步函式。
Babel 會為您完成這項工作,以下是透過 Babel REPL 的範例
- 請注意轉譯後的程式碼有多相似。這項轉換作業是 Babel 的 es2017 預設值的一部分。
我建議您採用轉譯方法,因為只要目標瀏覽器支援非同步函式,您就可以關閉轉譯,但如果您確實不想使用轉譯器,可以使用 Babel 的 polyfill,自行使用。而不是這樣
async function slowEcho(val) {
await wait(1000);
return val;
}
…您會加入polyfill 並寫入:
const slowEcho = createAsyncFunction(function* (val) {
yield wait(1000);
return val;
});
請注意,您必須將產生器 (function*
) 傳遞至 createAsyncFunction
,並使用 yield
而非 await
。除了這個差異,其他運作方式都相同。
解決方法:再生器
如果您指定舊版瀏覽器,Babel 也可以轉譯產生器,讓您在 IE8 以下版本中使用非同步函式。如要執行這項操作,您需要Babel 的 es2017 預設值 和es2015 預設值。
輸出內容不太美觀,因此請留意程式碼膨脹問題。
讓所有內容都以非同步方式處理!
非同步函式在所有瀏覽器上推出後,您就可以在每個承諾回傳函式中使用這些函式!這不僅可讓程式碼更整齊,還可確保函式「一律」傳回承諾。
在 2014 年,我對非同步函式感到非常興奮,很高興看到這些函式真的在瀏覽器中實現。太好了!