38. 异步函数

原文: http://exploringjs.com/impatient-js/ch_async-functions.html

译者:iChrisJ

粗略地说,异步函数 为使用 Promise 的代码提供了更好的语法。

38.1. 异步函数:基础知识

考虑以下异步函数:

async function fetchJsonAsync(url) {
  try {
    const request = await fetch(url); // async
    const text = await request.text(); // async
    return JSON.parse(text); // sync
  }
  catch (error) {
    assert.fail(error);
  }
}

上面看起来相似于同步的代码是相等于下面基于 Promise 的代码:

function fetchJsonViaPromises(url) {
  return fetch(url) // async
  .then(request => request.text()) // async
  .then(text => JSON.parse(text)) // sync
  .catch(error => {
    assert.fail(error);
  });
}

关于异步函数fetchJsonAsync()的一些观察:

  • 异步函数标有关键字async

  • 在异步函数体内,您可以编写基于 Promise 的代码,就好像它是同步的一样。只要其值为 Promise,您只需要添加await运算符。该运算符暂停异步函数并在 Promise 结算后恢复:

    • 如果那个 Promise 被履行,await将返回其履行值。
    • 如果那个 Promise 被拒绝,await会抛出其拒绝值。
  • 异步函数的结果始终是一个 Promise:

    • 返回的任何值(显式或隐式)用于实现 Promise。
    • 抛出的任何异常都用于拒绝 Promise。

fetchJsonAsync()fetchJsonViaPromises()都以完全相同的方式调用,如下所示:

fetchJsonAsync('http://example.com/person.json')
.then(obj => {
  assert.deepEqual(obj, {
    first: 'Jane',
    last: 'Doe',
  });
});

38.1.1. 异步构造

JavaScript 具有以下异步版本的同步可调用实体。他们的角色总是真实的函数或方法。

// Async function declaration
async function func1() {}

// Async function expression
const func2 = async function () {};

// Async arrow function
const func3 = async () => {};

// Async method definition (in classes, too)
const obj = { async m() {} };

38.1.2. 异步函数总是返回 Promise

每个异步函数总是返回一个 Promise。

在异步函数中,您通过return(A 行)履行 Promise 结果:

async function asyncFunc() {
  return 123; // (A)
}

asyncFunc()
.then(result => {
  assert.equal(result, 123);
});

像往常一样,如果您没有显式地返回任何内容,则会为您返回undefined

async function asyncFunc() {
}

asyncFunc()
.then(result => {
  assert.equal(result, undefined);
});

您通过throw(A 行)返回 Promise 被拒绝的结果 Promise:

let thrownError;
async function asyncFunc() {
  thrownError = new Error('Problem!');
  throw thrownError; // (A)
}

asyncFunc()
.catch(err => {
  assert.equal(err, thrownError);
});

38.1.3. 返回的 Promise 没有包装

如果从异步函数返回一个 Promise p,则p成为函数的结果(或者更确切地说,结果“锁定”在p上并且与它的行为完全相同)。也就是说,Promise 并不会包含在另一个 Promise 中。

async function asyncFunc() {
  return Promise.resolve('abc');
}

asyncFunc()
.then(result => assert.equal(result, 'abc'));

回想一下,在以下情况下,任何 Promise q都会被类似地处理:

  • new Promise((resolve, reject) => { ··· })内的resolve(q)
  • .then(result => { ··· })内的return q
  • .catch(err => { ··· })内的return q

38.1.4. await:与 Promise 一起工作

await运算符只能在异步函数中使用。它的操作对象通常是一个 Promise,并引导执行以下步骤:

  • 当前的异步功能被暂停(类似于使用yield同步生成器是如何暂停一样)。
  • 继续处理任务队列。
  • 一旦 Promise 得到解决,异步函数就会恢复:
    • 如果 Promise 被履行,await将返回履行值。
    • 如果 Promise 被拒绝,await会抛出拒绝值。

以下两节提供了更多详细信息。

38.1.5. await 与被履行的 Promise

如果其操作对象最终成为被履行的 Promise,await将返回其履行值:

assert.equal(await Promise.resolve('yes!'), 'yes!');

也允许非 Promise 值,并简单地传递(同步,没有异步函数的暂停):

assert.equal(await 'yes!', 'yes!');

38.1.6. await与被拒绝的 Promise

如果其操作对象是被拒绝的 Promise,则await会抛出拒绝值:

try {
  await Promise.reject(new Error());
  assert.fail(); // we never get here
} catch (e) {
  assert.equal(e instanceof Error, true);
}

Error的实例(包括其子类的实例)将被特别处理并抛出:

try {
  await new Error();
  assert.fail(); // we never get here
} catch (e) {
  assert.equal(e instanceof Error, true);
}

练习:Fetch API 通过异步函数

exercises/async-functions/fetch_json2_test.js

38.2. 术语

让我们澄清几个术语:

  • 异步函数异步方法:使用关键字async定义。异步函数也称为 async/await ,基于作为其语法基础的两个关键字。

  • 直接使用 Promise :意味着代码在没有await的情况下处理 Promise。

  • 基于 Promise 的:通过 Promises 提供结果和错误的函数或方法。也就是说,异步函数和返回 Promises 的函数都是合格的。

  • 异步:异步地传递结果和错误的函数或方法。这里,任何使用异步模式(回调,事件,Promise 等)的操作都是合格的。唉,事情有点令人困惑,因为“异步函数(async function)”中的“异步(async)”是“异步(asynchronous)”的缩写。

38.3. await很浅(你不能在回调中使用它)

如果你在异步函数中并希望通过await暂停它,则必须在该函数中执行此操作,不能在嵌套函数中使用它,例如回调。也就是说,暂停是

例如,以下代码无法执行:

async function downloadContent(urls) {
  return urls.map((url) => {
    return await httpGet(url); // SyntaxError!
  });
}

原因是普通箭头函数不允许await进入其函数体内。

好的,让我们尝试异步箭头函数,然后:

async function downloadContent(urls) {
  return urls.map(async (url) => {
    return await httpGet(url);
  });
}

唉,这也行不通:现在.map()(还有 downloadContent())返回一个带 Promise 的数组,而不是带有(未包装)值的数组。

一种可能的解决方案是使用Promise.all()解除所有 Promise 地包装:

async function downloadContent(urls) {
  const promiseArray = urls.map(async (url) => {
    return await httpGet(url); // (A)
  });
  return await Promise.all(promiseArray);
}

这段代码可以改进吗?是的,它可以,因为在 A 行,我们通过await展开 Promise,只是通过return立即重新包装它。我们可以省略await,然后甚至不需要异步箭头函数:

async function downloadContent(urls) {
  const promiseArray = urls.map(
    url => httpGet(url));
  return await Promise.all(promiseArray); // (B)
}

出于同样的原因,我们也可以省略 B 行中的await

练习:异步映射和过滤

exercises/async-functions/map_async_test.js

38.4. (高级)

所有剩余部分都是高级的。

38.5. 立即调用异步箭头函数

如果在异步函数外需要await(例如,在模块的顶层),则可以立即调用异步箭头函数:

(async () => { // start
  const promise = Promise.resolve('abc');
  const value = await promise;
  assert.equal(value, 'abc');
})(); // end

立即调用异步箭头函数的结果是一个 Promise:

const promise = (async () => 123)();
promise.then(x => assert.equal(x, 123));

38.6. 并发和await

38.6.1. await:顺序运行异步函数

如果你用await为多个异步函数的调用添加前缀,那么这些函数将按顺序执行:

const otherAsyncFunc1 = () => Promise.resolve('one');
const otherAsyncFunc2 = () => Promise.resolve('two');

async function asyncFunc() {
  const result1 = await otherAsyncFunc1();
  assert.equal(result1, 'one');

  const result2 = await otherAsyncFunc2();
  assert.equal(result2, 'two');
}

也就是说,otherAsyncFunc2()仅在otherAsyncFunc1()完全结束后才开始。

38.6.2. await:并发地运行异步函数

如果我们想同时运行多个函数,我们需要求助于工具方法Promise.all()

async function asyncFunc() {
  const [result1, result2] = await Promise.all([
    otherAsyncFunc1(),
    otherAsyncFunc2(),
  ]);
  assert.equal(result1, 'one');
  assert.equal(result2, 'two');
}

这里,两个异步函数同时启动。一旦两者都结算了,await给我们一个履行值数组或者 —— 如果至少有一个 Promise 被拒绝 —— 一个异常。

回想一下前一章,重要的是当你开始一个基于 Promise 的计算 - 而不是你如何处理它的结果。因此,以下代码与前一个代码一样“并发”:

async function asyncFunc() {
  const promise1 = otherAsyncFunc1();
  const promise2 = otherAsyncFunc2();

  const result1 = await promise1;
  const result2 = await promise2;

  assert.equal(result1, 'one');
  assert.equal(result2, 'two');
}

38.7. 使用异步功能的小技巧

38.7.1. 异步函数同步启动,异步结算

异步函数执行如下:

  • 当异步函数启动时会创建其结果的 Promise p
  • 然后函数体被执行。执行可以通过两种方式返回:
    • 执行可以永久返回,同时结算p
      • return一个已满足的p
      • throw一个被拒绝的p
    • 执行也可以暂时离开,当通过await等待另一个 Promise q的结算时。异步函数被暂停,执行离开它。一旦q被结算,它就会恢复。
  • Promise p在执行首次(永久或暂时)离开函数体后返回。

请注意,结果p的结算通知是异步发生的,Promise 的情况也是如此。

下面的代码演示了异步函数是同步启动的(行 A),然后当前任务完成(行 C),然后结果 Promise 得到解决 —— 异步(行 B)。

async function asyncFunc() {
  console.log('asyncFunc() starts'); // (A)
  return 'abc';
}
asyncFunc().
then(x => { // (B)
  console.log(`Resolved: ${x}`);
});
console.log('Task ends'); // (C)

// Output:
// 'asyncFunc() starts'
// 'Task ends'
// 'Resolved: abc'

38.7.2. 如果你是“触发后遗忘”那你不需要await

当使用一个基于 Promise 的函数时await不是必须的,如果要暂停并等到返回的 Promise 结算,则才需要它。如果你想要做的只是启动异步操作,那么你不需要它:

async function asyncFunc() {
  const writer = openFile('someFile.txt');
  writer.write('hello'); // don't wait
  writer.write('world'); // don't wait
  await writer.close(); // wait for file to close
}

在此代码中,我们不等待.write(),因为我们不关心它何时完成。但是,我们确实要等到.close()完成。

38.7.3. 它对await有意义并忽略结果

即使您忽略其结果,使用await偶尔也会有意义。例如:

await longRunningAsyncOperation();
console.log('Done!');

在这里,我们使用await加入长时间运行的异步操作。这确保了写日志确实发生操作完成


书籍推荐