让我们使用示例来了解 JavaScript 中的 Async/Await

本文作者研究了 JavaScript 中 Async/Await 的示例。 总的来说,Async/Await 是编写异步代码的一种便捷方式。 在此功能出现之前,此类代码是使用回调和 Promise 编写的。 原文章作者通过分析各种例子揭示了Async/Await的优点。

我们提醒: 对于“Habr”的所有读者 - 使用“Habr”促销代码注册任何 Skillbox 课程可享受 10 卢布的折扣。

技能箱推荐: 教育在线课程 《Java 开发人员》.

打回来

回调是一种无限期延迟调用的函数。 以前,回调用于那些无法立即获取结果的代码区域。

以下是 Node.js 中异步读取文件的示例:

fs.readFile(__filename, 'utf-8', (err, data) => {
  if (err) {
    throw err;
  }
  console.log(data);
});

当您需要同时执行多个异步操作时,就会出现问题。 让我们想象一下这样的场景:向 Arfat 用户数据库发出请求,您需要读取其 profile_img_url 字段并从 someserver.com 服务器下载图像。
下载后,我们将图像转换为其他格式,例如从 PNG 转换为 JPEG。 如果转换成功,则会向用户的电子邮件发送一封信。 接下来,有关事件的信息将输入到 conversions.log 文件中,并指示日期。

值得注意的是代码最后部分的回调重叠和大量})。 它被称为回调地狱或末日金字塔。

这种方法的缺点是显而易见的:

  • 这段代码很难阅读。
  • 错误处理也很困难,这通常会导致代码质量较差。

为了解决这个问题,JavaScript 中添加了 Promise。 它们允许您用 .then 一词替换回调的深度嵌套。

Promise 的积极方面是它们使代码从上到下而不是从左到右更具可读性。 然而,Promise 也有其问题:

  • 你需要添加很多.then。
  • .catch 代替 try/catch 来处理所有错误。
  • 在一个循环中使用多个 Promise 并不总是很方便;在某些情况下,它们会使代码变得复杂。

这里有一个问题将显示最后一点的含义。

假设我们有一个 for 循环,它以随机间隔(0-n 秒)打印从 10 到 0 的数字序列。 使用 Promise,您需要更改此循环,以便按从 0 到 10 的顺序打印数字。因此,如果打印 6 需要 2 秒,打印 XNUMX 需要 XNUMX 秒,则应先打印 XNUMX,然后再打印打印该文件的倒计时即将开始。

当然,我们不使用 Async/Await 或 .sort 来解决这个问题。 最后是一个示例解决方案。

异步函数

ES2017 (ES8) 中添加的异步函数简化了使用 Promise 的任务。 我注意到异步函数在承诺的“之上”工作。 这些功能并不代表本质上不同的概念。 异步函数旨在作为使用 Promise 的代码的替代方案。

Async/Await 使得以同步方式组织异步代码工作成为可能。

因此,了解 Promise 可以更轻松地理解 Async/Await 的原理。

句法

通常它由两个关键字组成:async 和await。 第一个字将函数转变为异步函数。 此类函数允许使用await。 在任何其他情况下,使用此函数都会产生错误。

// With function declaration
 
async function myFn() {
  // await ...
}
 
// With arrow function
 
const myFn = async () => {
  // await ...
}
 
function myFn() {
  // await fn(); (Syntax Error since no async)
}
 

Async 插入在函数声明的最开始处,如果是箭头函数,则插入在“=”号和括号之间。

这些函数可以作为方法放置在对象中,也可以在类声明中使用。

// As an object's method
 
const obj = {
  async getName() {
    return fetch('https://www.example.com');
  }
}
 
// In a class
 
class Obj {
  async getResource() {
    return fetch('https://www.example.com');
  }
}

注意! 值得记住的是,类构造函数和 getter/setter 不能是异步的。

语义和执行规则

异步函数与标准 JS 函数基本相似,但也有例外。

因此,异步函数总是返回 Promise:

async function fn() {
  return 'hello';
}
fn().then(console.log)
// hello

具体来说,fn 返回字符串 hello。 好吧,由于这是一个异步函数,因此使用构造函数将字符串值包装在 Promise 中。

这是没有异步的替代设计:

function fn() {
  return Promise.resolve('hello');
}
 
fn().then(console.log);
// hello

在这种情况下,promise 是“手动”返回的。 异步函数总是包含在新的 Promise 中。

如果返回值是原语,则异步函数通过将其包装在 Promise 中来返回该值。 如果返回值是一个 Promise 对象,则其解析结果将在新的 Promise 中返回。

const p = Promise.resolve('hello')
p instanceof Promise;
// true
 
Promise.resolve(p) === p;
// true
 

但是如果异步函数内部出现错误会发生什么?

async function foo() {
  throw Error('bar');
}
 
foo().catch(console.log);

如果没有被处理, foo() 将返回一个带有拒绝的承诺。 在这种情况下,将返回包含错误的 Promise.reject 而不是 Promise.resolve。

无论返回什么,异步函数总是输出一个承诺。

异步函数在每次等待时暂停。

Await 影响表达式。 因此,如果表达式是一个 Promise,则异步函数将被挂起,直到 Promise 得到履行。 如果表达式不是 Promise,则通过 Promise.resolve 将其转换为 Promise,然后完成。

// utility function to cause delay
// and get random value
 
const delayAndGetRandom = (ms) => {
  return new Promise(resolve => setTimeout(
    () => {
      const val = Math.trunc(Math.random() * 100);
      resolve(val);
    }, ms
  ));
};
 
async function fn() {
  const a = await 9;
  const b = await delayAndGetRandom(1000);
  const c = await 5;
  await delayAndGetRandom(1000);
 
  return a + b * c;
}
 
// Execute fn
fn().then(console.log);

这里是 fn 函数如何工作的描述。

  • 调用后,第一行由 const a = wait 9; 转换而来。 在 const a = wait Promise.resolve(9); 中。
  • 使用 Await 后,函数执行将暂停,直到 a 获取其值(在当前情况下为 9)。
  • delayAndGetRandom(1000) 暂停 fn 函数的执行,直到它自行完成(1 秒后)。 这实际上使 fn 函数停止 1 秒。
  • 通过resolve的delayAndGetRandom(1000)返回一个随机值,然后将其分配给变量b。
  • 那么,变量 c 的情况与变量 a 的情况类似。 之后,一切都会停止一秒钟,但现在delayAndGetRandom(1000)不会返回任何内容,因为它不是必需的。
  • 结果,使用公式 a + b * c 计算这些值。 结果使用 Promise.resolve 包装在 Promise 中并由函数返回。

这些停顿可能会让人想起 ES6 中的生成器,但它确实有一些东西 你的理由.

解决问题

好了,现在我们来看看上面提到的问题的解决方案。

finishMyTask 函数使用 Await 来等待 queryDatabase、sendEmail、logTaskInFile 等操作的结果。 如果将此解决方案与使用 Promise 的解决方案进行比较,相似之处就会变得显而易见。 然而,Async/Await 版本极大地简化了所有语法复杂性。 这种情况下,就没有像.then/.catch这样大量的回调和链了。

这是一个输出数字的解决方案,有两种选择。

const wait = (i, ms) => new Promise(resolve => setTimeout(() => resolve(i), ms));
 
// Implementation One (Using for-loop)
const printNumbers = () => new Promise((resolve) => {
  let pr = Promise.resolve(0);
  for (let i = 1; i <= 10; i += 1) {
    pr = pr.then((val) => {
      console.log(val);
      return wait(i, Math.random() * 1000);
    });
  }
  resolve(pr);
});
 
// Implementation Two (Using Recursion)
 
const printNumbersRecursive = () => {
  return Promise.resolve(0).then(function processNextPromise(i) {
 
    if (i === 10) {
      return undefined;
    }
 
    return wait(i, Math.random() * 1000).then((val) => {
      console.log(val);
      return processNextPromise(i + 1);
    });
  });
};

这是使用异步函数的解决方案。

async function printNumbersUsingAsync() {
  for (let i = 0; i < 10; i++) {
    await wait(i, Math.random() * 1000);
    console.log(i);
  }
}

错误处理

未处理的错误包含在被拒绝的承诺中。 但是,异步函数可以使用 try/catch 来同步处理错误。

async function canRejectOrReturn() {
  // wait one second
  await new Promise(res => setTimeout(res, 1000));
 
// Reject with ~50% probability
  if (Math.random() > 0.5) {
    throw new Error('Sorry, number too big.')
  }
 
return 'perfect number';
}

canRejectOrReturn() 是一个异步函数,它要么成功(“完美数字”),要么失败并出现错误(“抱歉,数字太大”)。

async function foo() {
  try {
    await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

由于上面的示例期望 canRejectOrReturn 执行,因此它本身的失败将导致 catch 块的执行。 结果,函数 foo 将以未定义(当 try 块中没有返回任何内容时)或捕获错误结束。 因此,该函数不会失败,因为 try/catch 将处理函数 foo 本身。

这是另一个例子:

async function foo() {
  try {
    return canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

值得注意的是,在示例中,canRejectOrReturn 是从 foo 返回的。 在这种情况下,Foo 要么以完美数字终止,要么返回错误(“抱歉,数字太大”)。 catch 块永远不会被执行。

问题是 foo 返回从 canRejectOrReturn 传递的承诺。 所以 foo 的解决方案就变成了 canRejectOrReturn 的解决方案。 在这种情况下,代码将仅包含两行:

try {
    const promise = canRejectOrReturn();
    return promise;
}

如果同时使用 wait 和 return ,会发生以下情况:

async function foo() {
  try {
    return await canRejectOrReturn();
  } catch (e) {
    return 'error caught';
  }
}

在上面的代码中, foo 将成功退出,并捕获一个完全数和一个错误。 这里不会有人拒绝。 但 foo 将以 canRejectOrReturn 返回,而不是 undefined。 让我们通过删除 return wait canRejectOrReturn() 行来确保这一点:

try {
    const value = await canRejectOrReturn();
    return value;
}
// …

常见错误和陷阱

在某些情况下,使用 Async/Await 可能会导致错误。

被遗忘的等待

这种情况经常发生 - 在承诺之前忘记了await关键字:

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}

正如你所看到的,代码中没有await或return。 因此 foo 总是以 undefined 退出,没有 1 秒的延迟。 但承诺一定会实现。 如果它抛出错误或拒绝,则将调用 UnhandledPromiseRejectionWarning。

回调中的异步函数

异步函数经常在 .map 或 .filter 中用作回调。 一个例子是 fetchPublicReposCount(username) 函数,它返回 GitHub 上打开的存储库的数量。 假设我们需要三个用户的指标。 这是此任务的代码:

const url = 'https://api.github.com/users';
 
// Utility fn to fetch repo counts
const fetchPublicReposCount = async (username) => {
  const response = await fetch(`${url}/${username}`);
  const json = await response.json();
  return json['public_repos'];
}

我们需要 ArfatSalman、octocat、norvig 帐户。 在这种情况下,我们这样做:

const users = [
  'ArfatSalman',
  'octocat',
  'norvig'
];
 
const counts = users.map(async username => {
  const count = await fetchPublicReposCount(username);
  return count;
});

值得关注 .map 回调中的 Await。 这里 counts 是一个 Promise 数组,.map 是每个指定用户的匿名回调。

过度一致地使用await

我们以这段代码为例:

async function fetchAllCounts(users) {
  const counts = [];
  for (let i = 0; i < users.length; i++) {
    const username = users[i];
    const count = await fetchPublicReposCount(username);
    counts.push(count);
  }
  return counts;
}

这里,回购编号被放置在 count 变量中,然后该数字被添加到 counts 数组中。 该代码的问题在于,在第一个用户的数据从服务器到达之前,所有后续用户都将处于待机模式。 因此,一次仅处理一个用户。

例如,如果处理一个用户大约需要 300 毫秒,那么对于所有用户来说已经是一秒了;所花费的时间线性取决于用户数量。 但由于获取repo的数量并不依赖于彼此,所以这些过程可以并行化。 这需要使用 .map 和 Promise.all:

async function fetchAllCounts(users) {
  const promises = users.map(async username => {
    const count = await fetchPublicReposCount(username);
    return count;
  });
  return Promise.all(promises);
}

Promise.all 接收一组 Promise 作为输入并返回一个 Promise。 后者在数组中的所有承诺完成后或在第一次拒绝时完成。 可能会发生它们不是同时启动的情况——为了确保同时启动,可以使用p-map。

结论

异步函数对于开发变得越来越重要。 那么,为了自适应使用异步函数,您应该使用 异步迭代器。 JavaScript 开发人员应该精通这一点。

技能箱推荐:

来源: habr.com

添加评论