Async 函數 —— 讓 promise 更友好

Rut1134 8年前發布 | 18K 次閱讀 Promise JavaScript開發

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

 

 本文由用戶 Rut1134 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!