非同步函式可讓您編寫以承諾為基準的程式碼,就像同步作業一樣。
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 的「Welder of Promiss」(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 迴圈,進一步簡化。
其他非同步函式語法
我展示了 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 async 函式。
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 年推出非同步函式,很高興看到這些功能確實能在瀏覽器上順利上線。太好了!