Async 函數 —— 讓 promise 更友好
Async 函數--讓 promise 更友好
Async 函數在 Chrome 55 中是默認開啟的, 它確實非常不錯。它允許你寫出看起來像是同步的、基于 promise 的代碼,但不會阻塞主線程。它讓你的異步代碼沒那么“聰明”和更加可讀。
Async 函數像這樣工作:
async function myFirstAsyncFunction() {
try {
const fulfilledValue = await promise;
}
catch (rejectedValue) {
// …
}
}
如果你在一個函數定義前使用 async 關鍵字,你就可以在函數內部使用 await 。當你 await (等待) 一個 promise,該函數會以非阻塞的方式中斷,直到 promise 被解決。如果 promise 完成,你就可以得到結果。如果 promise 拒絕,就會拋出被拒絕的值。
示例:記錄數據獲取
假設我們想獲取一個 URL 并記錄響應的文本。這里是使用 promise 的方式:
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);
}
}
代碼行數是一樣的,但是所有的回調都不見了。這讓它更容易閱讀,特別是對那些不太熟悉 promise的人來說。
注意:你 await (等待)的所有東西都是通過 Promise.resolve() 傳遞的,因此你可以安全地 await (等待)非本地的 promise。
Async 返回值
Async 函數總是會返回一個 promise,不管你是否用了 await 。這個 promise 用 async 函數返回的任何值來解決,或者用 async 函數拋出的任何值來拒絕。因此給定如下代碼:
// wait ms milliseconds
function wait(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function hello() {
await wait(500);
return 'world';
}</code></pre>
調用 hello() 會返回一個用 "world" 來完成的 promise。
async function foo() {
await wait(500);
throw Error('bar');
}
調用 foo() 會返回一個用 Error('bar') 來拒絕的 promise。
示例:響應流
在更復雜的例子中 async 函數的好處更多。假設我們想在記錄響應數據片段時將其變成數據流,并返回最終的大小。
注意:“記錄片段” 這句話讓我感到不適。
用 promise 是這樣的:
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);
})
});
}</code></pre>
看清楚了,我是 promise “地下黨” Jake Archibald。看到我是怎樣在它內部調用 processResult 并建立異步循環的了嗎?這樣寫讓我覺得自己“很聰明”。但是正如大多數“聰明的”代碼一樣,你不得不盯著它看很久才能搞清楚它在做什么,就像九十年代的那些魔眼照片一樣。
讓我們再用 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;
}</code></pre>
所有的“小聰明”都不見了。讓我自鳴得意的異步循環被一個可信任的、枯燥的 while 循環替代。好多了。將來,我們還有 async 迭代器 ,將會 用 for-of 循環替換 while 循環 ,這樣就更好了。
注意:我有點喜歡數據流。
async 函數的其他語法
我們已經見過 async function() {} 了,但是 async 關鍵字還可以在其他的函數語法里使用:
箭頭函數
// map some URLs to json-promises
const jsonPromises = urls.map(async url => {
const response = await fetch(url);
return response.json();
});
注意: array.map(func) 并不在乎我給它傳的是 async 函數,它只是把它當做一個返回 promise 的函數。它在調用第二個函數之前并不會等待第一個函數完成。
對象方法
const storage = {
async getAvatar(name) {
const cache = await caches.open('avatars');
return cache.match(/avatars/${name}.jpg
);
}
};
storage.getAvatar('jaffathecake').then(…);</code></pre>
類方法
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(…);</code></pre>
注意:類的構造函數和 getters/settings 不能是 async。
小心!避免過度強制先后順序
盡管你寫的代碼看起來是同步的,要確保不要錯過并行處理的機會。
async function series() {
await wait(500);
await wait(500);
return "done!";
}
執行以上代碼要 1000 毫秒,而:
async function parallel() {
const wait1 = wait(500);
const wait2 = wait(500);
await wait1;
await wait2;
return "done!";
}
以上代碼 500 毫秒完成,因為兩個 wait 是同時發生的。讓我們看看一個實際的例子。
例子:按順序輸出 fetch
假設我們想獲取一系列的 URL 并盡快地按正確的順序記錄下來。
深呼吸 —— 用 promise 看起來是這樣的:
function logInOrder(urls) {
// fetch all the URLs
const textPromises = urls.map(url => {
return fetch(url).then(response => response.text());
});
// log them in order
textPromises.reduce((chain, textPromise) => {
return chain.then(() => textPromise)
.then(text => console.log(text));
}, Promise.resolve());
}</code></pre>
是的,沒錯,我在用 reduce 把一系列的 promise 串起來了。我“太聰明了”。但這是我們不應該有的“如此聰明的”代碼。
然而,當我們把上面的代碼轉成 async 函數時,它變得 過于強調先后順序了 :
不推薦 —— 太強制先后順序了
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
看起來更簡潔了,但是在第一個數據獲取完全被讀取前,第二個數據獲取不會開始,后續的也是一樣。這會比并發獲取數據的 promise 例子慢得多。幸好還有一個理想的折中方案:
推薦 —— 很好,并行
async function logInOrder(urls) {
// fetch all the URLs in parallel
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// log them in sequence
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}</code></pre>
在這個例子中,URL 是并行獲取和讀取的,但是“聰明的” reduce 被一個標準的、枯燥的、可讀性好的 for 循環取代了。
瀏覽器支持情況和變通方案
截止到本文寫作時,Chrome 55 默認支持 async 函數,但所有主流瀏覽器都在開發中:
-
Edge - 版本 build 14342+ 加上 flag
-
Firefox - 活躍開發中
-
Safari - 活躍開發中
變通方案 - Generators
如果你的目標瀏覽器支持生成器,你可以模擬 async 函數。
Babel 可以幫你做到, 這里有個使用 Babel REPL 的例子 —— 注意看下轉換后的代碼多么相似。該轉換是 Babel 的 es2017 預設版 。
注意:Babel REPL 發音很有趣。試試看。
我推薦使用轉換的方式,因為一旦你的目標瀏覽器支持 async 函數了,你就可以關掉轉換。不過如果你確實不想用轉換器,你可以用 Babel 墊片 。不用這么寫:
async function slowEcho(val) {
await wait(1000);
return val;
}
你可以引入 這個墊片 并這樣寫:
const slowEcho = createAsyncFunction(function*(val) {
yield wait(1000);
return val;
});
注意,你必須傳遞一個生成器 ( function* ) 到 createAsyncFunction ,并使用 yield 而不是 await 。除了這里,效果是一樣的。
變通方案 —— 生成器轉換
如果你需要支持較老的的瀏覽器,Babel 也可以轉換生成器,允許你在低至 IE8 上使用 async 函數。為此你需要 Babel es2017 預設 以及 es2015 預設 。
輸出結果沒那么好看 ,所以小心代碼膨脹。
Async 一切!
一旦所有瀏覽器都可以用 async 函數的時候,在所有返回 promise 的函數里使用它!它不僅讓你的代碼更整潔,而且能確保函數總是返回一個 promise。
我 早在2014年的時候 就對 async 函數非常興奮了,看到它真正地落實到瀏覽器里,感覺非常棒。啊!
除非另外說明,本頁的內容遵守 Creative Commons Attribution 3.0 License ,代碼示例遵守 Apache 2.0 License 。
來自:http://www.zcfy.cc/article/async-functions-making-promises-friendly-1566.html