异步函数:使 Promise 友好

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

Jake Archibald
Jake Archibald

Chrome、Edge、Firefox 和 Safari 默认启用异步函数,并且 他们真的很了不起它们允许您将基于 promise 的代码编写为 如果它是同步的,但不阻塞主线程。它们使您的 异步代码不太“聪明”更易于阅读

异步函数的工作方式如下:

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

如果您在函数定义前使用 async 关键字,则可以使用 await 函数。当您对 promise 执行 await 操作后,函数会暂停 以非阻塞方式运行,直到 promise 产生结果。如果 promise 执行, 获得相应的价值如果 promise 拒绝,则会抛出拒绝的值。

浏览器支持

浏览器支持

  • Chrome:55。 <ph type="x-smartling-placeholder">
  • Edge:15。 <ph type="x-smartling-placeholder">
  • Firefox:52。 <ph type="x-smartling-placeholder">
  • Safari:10.1. <ph type="x-smartling-placeholder">

来源

示例:记录提取

假设您想要提取网址并以文本形式记录响应。外观如下 使用 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,“wielder of promise”阿奇巴尔德。查看我的致电方式 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 循环。好多了。以后,您将获得 异步迭代器 这会 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 毫秒才能完成,因为两个等待时间同时发生。 我们来看一个实际示例。

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

假设您想在 顺序正确。

深呼吸 - 以下是使用 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 年以及 很高兴看到它们真实地出现在浏览器中哎呀!