写给心急程序员的 JavaScript(ES2022 版)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

41 异步函数



粗略地说,*异步函数* 为使用 Promise 的代码提供了更好的语法。因此,为了使用异步函数,我们应该了解 Promise。它们在上一章中进行了说明。

41.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() 的几点观察

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

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

  异步函数与直接使用 Promise 的函数一样基于 Promise

从外部看,几乎不可能区分异步函数和返回 Promise 的函数。

41.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 an object literal
const obj = { async m() {} };

// Async method definition in a class definition
class MyClass { async m() {} }

  异步函数与异步函数

*异步函数* 和 *异步函数* 这两个术语之间的区别很微妙,但很重要

41.2 从异步函数返回值

41.2.1 异步函数始终返回 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

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

asyncFunc()
.catch(err => {
  assert.deepEqual(err, new Error('Problem!'));
});

41.2.2 返回的 Promise 不会被包装

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

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

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

回想一下,在以下情况下,任何 Promise q 的处理方式都类似

41.2.3 执行异步函数:同步开始,异步完成(高级)

异步函数的执行方式如下

请注意,结果 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'

41.3 await:使用 Promise

await 运算符只能在异步函数和异步生成器(在§42.2 “异步生成器”中解释)内部使用。它的操作数通常是一个 Promise,并会导致执行以下步骤

继续阅读以了解更多关于 await 如何处理处于各种状态的 Promise 的信息。

41.3.1 await 和已完成的 Promise

如果它的操作数最终是一个已完成的 Promise,则 await 返回其完成值

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

也允许使用非 Promise 值,并且只需传递它们(同步地,不暂停异步函数)

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

41.3.2 await 和已拒绝的 Promise

如果它的操作数是一个被拒绝的 Promise,则 await 抛出拒绝值

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

  练习:通过异步函数获取 API

exercises/async-functions/fetch_json2_test.mjs

41.3.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

41.3.4 在模块的顶层使用 await [ES2022]

我们可以在模块的顶层使用 await,例如

let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}

有关此功能的更多信息,请参阅§27.14 “模块中的顶层 await [ES2022]”

  练习:异步映射和过滤

exercises/async-functions/map_async_test.mjs

41.4 (高级)

所有剩余部分都是高级内容。

41.5 并发和 await

接下来的两个小节中,我们将使用辅助函数 paused()

/**
 * Resolves after `ms` milliseconds
 */
function delay(ms) {
  return new Promise((resolve, _reject) => {
    setTimeout(resolve, ms);
  });
}
async function paused(id) {
  console.log('START ' + id);
  await delay(10); // pause
  console.log('END ' + id);
  return id;
}

41.5.1 await:顺序运行异步函数

如果我们使用 await 作为多个异步函数调用的前缀,则这些函数将顺序执行

async function sequentialAwait() {
  const result1 = await paused('first');
  assert.equal(result1, 'first');
  
  const result2 = await paused('second');
  assert.equal(result2, 'second');
}

// Output:
// 'START first'
// 'END first'
// 'START second'
// 'END second'

也就是说,只有在 paused('first') 完全完成后才会启动 paused('second')

41.5.2 await:并发运行异步函数

如果我们想并发运行多个函数,可以使用工具方法 Promise.all()

async function concurrentPromiseAll() {
  const result = await Promise.all([
    paused('first'), paused('second')
  ]);
  assert.deepEqual(result, ['first', 'second']);
}

// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'

在这里,两个异步函数同时启动。一旦两者都完成,await 就会给我们一个完成值的数组,或者——如果至少有一个 Promise 被拒绝——一个异常。

回想一下§40.6.2 “并发技巧:关注操作何时开始”,重要的是我们何时开始基于 Promise 的计算;而不是我们如何处理其结果。因此,以下代码与前一段代码一样“并发”

async function concurrentAwait() {
  const resultPromise1 = paused('first');
  const resultPromise2 = paused('second');
  
  assert.equal(await resultPromise1, 'first');
  assert.equal(await resultPromise2, 'second');
}
// Output:
// 'START first'
// 'START second'
// 'END first'
// 'END second'

41.6 使用异步函数的技巧

41.6.1 如果我们“触发即忘记”,则不需要 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() 完成。

注意:每次调用 .write() 都是同步开始的。这可以防止出现竞争条件。

41.6.2 await 并忽略结果是有意义的

有时,即使我们忽略 await 的结果,使用它也是有意义的,例如

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

在这里,我们使用 await 来加入一个长时间运行的异步操作。这确保了日志记录确实发生在该操作*之后*。