非同步函式可讓您編寫承諾型程式碼,就像是同步程式碼一樣。
根據預設,Chrome、Edge、Firefox 和 Safari 都會啟用非同步函式,而且功能非常驚人。可讓您在不封鎖主執行緒的情況下,編寫以承諾為基礎的程式碼,如同同步作業一般。這些範例會讓非同步程式碼感覺不「簡潔」,而且更容易判讀。
Async 函式的運作方式如下:
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 函式也是一樣:
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);
});
});
}
快來看看我說的「勝利的真相」。Archibald看看我如何在內部呼叫 processResult()
,以設定非同步迴圈?讓寫作感覺非常聰明。但就像大多數的「智慧型」程式碼一樣,您必須對它看待它,以便年齡瞭解其運作,例如 90 年代的其中一張神奇的相片。
請使用 async 函式再試一次:
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 迴圈,使其更加難以理解。
其他非同步函式語法
我已經顯示過 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 預設設定。
輸出內容不等於美,因此請留意程式碼 Bloat。
將所有事物非同步!
非同步函式在所有瀏覽器中運作後,請針對每個承諾傳回函式使用這個函式!這麼做不僅可以讓程式碼更易於使用,還能確保該函式「一律」會傳回 promise。
我很期待 2014 年的非同步函式,這些函式真的能在瀏覽器中產生,令我非常滿意。太好了!