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

非同步函式可讓您編寫承諾型程式碼,就像是同步程式碼一樣。

阿奇巴德 (Jake Archibald)
Jake Archibald

根據預設,Chrome、Edge、Firefox 和 Safari 都會啟用非同步函式,而且功能非常驚人。可讓您在不封鎖主執行緒的情況下,編寫以承諾為基礎的程式碼,如同同步作業一般。這些範例會讓非同步程式碼感覺不「簡潔」,而且更容易判讀。

Async 函式的運作方式如下:

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 函式也是一樣:

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);
  }
}
在這個範例中,系統會平行擷取及讀取網址,但「智慧型」reduce 位元會替換為標準、無聊、可讀的迴圈。

瀏覽器支援解決方法:產生器

如果您指定的瀏覽器支援產生器 (其中包含所有主要瀏覽器的最新版本),您可以使用 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 預設設定

輸出內容不等於美,因此請留意程式碼 Bloat。

將所有事物非同步!

非同步函式在所有瀏覽器中運作後,請針對每個承諾傳回函式使用這個函式!這麼做不僅可以讓程式碼更易於使用,還能確保該函式「一律」會傳回 promise。

我很期待 2014 年的非同步函式,這些函式真的能在瀏覽器中產生,令我非常滿意。太好了!