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

非同步函式可讓您編寫以承諾為基準的程式碼,就像同步作業一樣。

Jake Archibald
Jake Archibald

Chrome、Edge、Firefox 和 Safari 預設會啟用非同步功能,這些功能相當奇妙。這類元件可讓您撰寫以保證為基礎的程式碼,就像同步執行一樣,但不會封鎖主執行緒。這些非同步程式碼會使非同步程式碼變得「聰明」且更容易閱讀。

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

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

如果您在函式定義之前使用 async 關鍵字,可以在函式中使用 await。當您 await 承諾時,函式會以非阻塞的方式暫停,直到承諾和值為止。如果保證符合承諾 就會退還差額如果承諾產品遭拒,系統就會擲回遭拒的值。

瀏覽器支援

瀏覽器支援

  • 55
  • 15
  • 52
  • 10.1

來源

範例:記錄擷取作業

假設您想擷取網址並將回應記錄為文字。其使用承諾如下:

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);
  }
}
在這個例子中,系統會同時擷取及讀取網址,但會將「智慧」reduce 位元替換成可讀取的標準亂碼。

瀏覽器支援的替代方案:產生器

如果您指定的瀏覽器支援產生器 (包括所有主要瀏覽器的最新版本),您可以排序 polyfill async 函式。

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 年推出非同步函式,很高興看到這些功能確實能在瀏覽器上順利上線。太好了!