异步函数:使 Promise 友好

异步函数让您能像编写同步代码一样编写基于 Promise 的代码。

杰克·阿奇博尔德
Jake Archibald

Chrome、Edge、Firefox 和 Safari 默认会启用异步函数,坦率地说,它们的作用相当不可思议。利用这些方法,您可以像编写同步代码一样编写基于 promise 的代码,同时又不会阻塞主线程。它们会使异步代码不那么“智能”且可读性更高。

异步函数的工作原理如下:

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

如果您在函数定义前面使用了 async 关键字,就可以在函数中使用 await。当您对某个 promise 执行 await 时,函数会以非阻塞方式暂停,直到该 promise 产生结果。如果 promise 执行,则会返回值。如果 promise 拒绝,则会抛出被拒值。

浏览器支持

浏览器支持

  • 55
  • 15
  • 52 页
  • 10.1

来源

示例:记录提取

假设您想要提取网址并以文本形式记录响应。使用 promise 后的代码如下所示:

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);
  }
}

虽然行数相同,但所有回调都消失了。这使其更易于阅读,尤其是对于不太熟悉 Promise 的人。

异步返回值

无论您是否使用 await,异步函数始终都会返回一个 promise。该 promise 会在解析时使用异步函数返回的任何内容,或在拒绝时返回异步函数抛出的任何内容。因此,对于:

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

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

...调用 hello() 会返回一个使用 "world" 执行的 promise。

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

...调用 foo() 会返回一个使用 Error('bar') 拒绝的 promise。

示例:流式传输响应

在更复杂的示例中,异步函数的优势更加突出。假设您想在流式传输响应的同时记录数据块的日志,然后返回最终大小。

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

来吧,“许诺的持有者”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 循环,使其更加简洁。

其他异步函数语法

我已经为您介绍过 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!';
}

以上代码需要 1, 000 毫秒才能完成,其中:

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 毫秒才能完成,因为两个等待是同时发生的。我们来看一个实际示例。

示例:按顺序输出提取内容

假设您想要提取一系列网址并尽快以正确顺序记录它们。

深呼吸 - 使用 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 function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
看起来简洁得多,但我的第二次提取要等到第一次提取都被完全读取后才开始,以此类推。这比并行执行提取的 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);
  }
}
在此示例中,系统会并行提取和读取网址,但“智能”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 预设

输出不够美观,因此请留意代码膨胀。

全面异步化!

一旦异步函数在所有浏览器上加载,就将其用于每个返回 promise 的函数!它们不仅能使代码更加简洁,还能确保函数始终会返回 promise。

早在 2014 年就对异步函数感到非常兴奋,并且很高兴看到它们真正登陆浏览器。哎呀!