Async 函数:让 Promise 更友好

Async 函数允许您像编写同步代码一样编写基于 Promise 的代码。

Async 函数在 Chrome、Edge、Firefox 和 Safari 中默认启用,而且坦率地说,它们非常出色。它们允许您像编写同步代码一样编写基于 Promise 的代码,但不会阻塞主线程。它们使您的异步代码不那么“巧妙”,更易于阅读。

Async 函数的工作方式如下

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

如果您在函数定义之前使用 async 关键字,则可以在函数内使用 await。当您 await 一个 Promise 时,函数会以非阻塞方式暂停,直到 Promise 变为 settled 状态。如果 Promise resolve,您将获得返回值。如果 Promise reject,则会抛出 reject 的值。

浏览器支持

浏览器支持

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

来源

示例:记录 fetch

假设您想 fetch 一个 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 的人来说。

Async 返回值

Async 函数*始终*返回 Promise,无论您是否使用 await。该 Promise 会 resolve 为 async 函数返回的任何值,或 reject 为 async 函数抛出的任何值。因此,对于

// wait ms milliseconds
function wait(ms) {
  return new Promise((r) => setTimeout(r, ms));
}

async function hello() {
  await wait(500);
  return 'world';
}

…调用 hello() 返回一个 Promise,该 Promise resolve 为 "world"

async function foo() {
  await wait(500);
  throw Error('bar');
}

…调用 foo() 返回一个 Promise,该 Promise reject 为 Error('bar')

示例:流式处理响应

Async 函数的优势在更复杂的示例中会更加明显。假设您想在记录 chunks 的同时流式处理响应,并返回最终大小。

这是使用 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);
    });
  });
}

看看我,Promise 的“掌控者” Jake 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 循环所取代。 好多了。 未来,您将获得 async iterators,它将 `while` 循环替换为 for-of 循环,使其更加简洁。

其他 Async 函数语法

我已经向您展示了 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 毫秒才能完成,因为两个等待同时发生。 让我们看一个实际的例子。

示例:按顺序输出 fetch

假设您想 fetch 一系列 URL 并尽快按正确的顺序记录它们。

深呼吸 - 这是使用 Promise 的方式

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 来链接一系列 Promise。 我 *太聪明了*。 但是,这种 *太聪明* 的编码最好还是不用。

但是,当将上述代码转换为 async 函数时,很容易变得 *过于串行*

不推荐 - 过于串行
async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
看起来更简洁,但我的第二个 fetch 要等到我的第一个 fetch 完全读取完毕后才开始,依此类推。 这比并行执行 fetch 的 Promise 示例慢得多。 幸运的是,有一个理想的折衷方案。
推荐 - 既好又并行
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);
  }
}
在此示例中,URL 是并行 fetch 和读取的,但是“巧妙”的 reduce 部分被一个标准的、枯燥的、可读的 for 循环所取代。

浏览器支持的变通方法:generators

如果您定位的浏览器支持 generators(包括 每个主要浏览器的最新版本),您可以某种程度上 polyfill async 函数。

Babel 会为您执行此操作,这是一个通过 Babel REPL 的示例

我推荐 transpiling 方法,因为一旦您的目标浏览器支持 async 函数,您就可以将其关闭,但是如果您 *真的* 不想使用 transpiler,您可以采用 Babel 的 polyfill 并自行使用它。 而不是

async function slowEcho(val) {
  await wait(1000);
  return val;
}

…您将包含 polyfill 并编写

const slowEcho = createAsyncFunction(function* (val) {
  yield wait(1000);
  return val;
});

请注意,您必须将 generator (function*) 传递给 createAsyncFunction,并使用 yield 而不是 await。 除此之外,它的工作方式相同。

变通方法:regenerator

如果您定位的是较旧的浏览器,Babel 也可以 transpile generators,使您可以一直使用 async 函数到 IE8。 为此,您需要 Babel 的 es2017 presetes2015 preset

输出结果不是那么漂亮,因此请注意代码膨胀。

Async 一切!

一旦 async 函数在所有浏览器中落地,请在每个返回 Promise 的函数上使用它们! 它们不仅使您的代码更简洁,而且可以确保该函数 *始终* 返回 Promise。

早在 2014 年,我就对 async 函数感到非常兴奋,很高兴看到它们真正在浏览器中落地。 哇呼!