非同步函式:讓承諾易於使用

非同步函式可讓您以同步方式編寫以承諾為基礎的程式碼。

Jake Archibald
Jake Archibald

根據預設,Chrome、Edge、Firefox 和 Safari 都會啟用非同步函式,而且這些函式相當出色。您可以使用這些方法,以同步方式編寫以承諾為基礎的程式碼,但不會阻斷主執行緒。這類程式碼可讓非同步程式碼變得更「聰明」,也更易於閱讀。

非同步函式的運作方式如下:

async function myFirstAsyncFunction() {   try {     const fulfilledValue = await promise;   } catch (rejectedValue) {     // …   } } 

如果您在函式定義前使用 async 關鍵字,就可以在函式中使用 awaitawait 承諾時,函式會以非阻塞的方式暫停,直到承諾解決為止。如果應許已兌現,您就會收到該值。如果應許遭到拒絕,系統會擲回遭拒絕的值。

瀏覽器支援

Browser Support

  • Chrome: 55.
  • Edge: 15.
  • Firefox: 52.
  • Safari: 10.1.

Source

範例:記錄擷取作業

假設您想擷取網址,並將回應記錄為文字。以下是使用承諾的情況:

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') rejects

範例:串流回應

在更複雜的範例中,非同步函式的優勢會更加明顯。假設您想在記錄區塊時串流回應,並傳回最終大小。

以下是使用承諾的範例:

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());   } }
看起來更整齊,但第二個擷取作業必須等到第一個擷取作業完全讀取後才會開始,以此類推。這比並行執行擷取作業的承諾範例慢上許多。所幸,我們有理想的中庸之道。
建議 - 使用 nice 和平行處理
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);   } }
在這個範例中,系統會並行擷取及讀取網址,但「聰明」的 reduce 位元會替換為標準、無聊且可讀的 for 迴圈。

瀏覽器支援因應措施:產生器

如果您指定支援產生器的瀏覽器 (包括各大瀏覽器的最新版本),就可以使用類似 polyfill 的非同步函式。

Babel 會為您完成這項工作,以下是透過 Babel REPL 的範例

我建議使用轉譯方法,因為只要目標瀏覽器支援非同步函式,您就可以關閉轉譯,但如果您真的不想使用轉譯器,可以使用 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 年,我對非同步函式感到非常興奮,很高興看到這些函式真的在瀏覽器中實現。太好了!