【译】深刻理解 JavaScript 中的 Async and Await

原文:blog.bitsrc.io/understandi…
做者:Arfat Salman
翻译:前端小白javascript

首先咱们来讨论下回调函数,回调函数没什么特别的,只是在未来的某个时候执行的函数。因为JavScript的异步特性,在许多不能当即得到结果的地方都须要回调前端

这是一个Node.js异步读取文件的例子:java

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

当咱们想要执行多个异步操做时,就会出现问题。想象如下的场景(全部操做都是异步的):git

  • 咱们在数据库中查询用户 Arfat。 咱们读取 profile_img_url 并从 someServer.com 获取图像。
  • 获取图像后,咱们将其转换为另外一种格式,好比PNG到JPEG。
  • 若是转换成功,咱们将向用户发送电子邮件。
  • transformations.log 中记录这个任务,并带上时间戳

代码大体以下:es6

回调地狱

注意回调函数的嵌套的末尾 }) 的层级关系,这种方式被戏称做 回调地狱回调金字塔。缺点是 ——github

  • 代码可读性太差
  • 错误处理很复杂,经常致使错误代码。

为了解决上述问题,JavaScript 提出了 Promise。如今,咱们可使用链式结构而不是回调函数嵌套的结构。下面是一个例子 ——数据库

使用promise

如今整个流程是自上而下而不是从左至右结构,这是一个优势。可是 promise 仍然有一些缺点 ——json

  • 在每一个 .then 中咱们仍是要处理回调
  • 相比使用正常的 try/catch,咱们要使用 .catch() 处理错误
  • 在循环中按顺序处理多个 promises 会很是头疼,不直观

咱们来演示下关于最后一个缺点:api

挑战

假设咱们有一个for循环,它以随机间隔(0到n秒)打印0到10。咱们须要使用 promise 按0到10顺序打印出来。例如,若是0打印须要6秒,1打印须要2秒,那么1应该等待0打印完以后再打印,以此类推。数组

不要使用 async/await 或者 .sort,以后咱们会解决这个问题

Async 函数

ES2017(ES8) 中引入了async函数,使promise应用起来很是简单

  • 很重要的一点须要注意:async 函数的使用是基于 promise
  • 他们不是彻底不一样的概念
  • 能够认为是一种基于 promise 异步代码的替代方案
  • async/await 能够避免使用 promise 链式调用
  • 代码异步执行,看起来是同步式的

所以,理解 async/await 必需要先了解 promise

语法

async/await 包含两个关键字 asyncawaitasync 用来使得函数能够异步执行。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 关键字在函数声明的前面。在箭头函数中,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');
  }
}
复制代码

注意:类的构造函数和 getters/setters 不能使用 async 函数。

语义和评估准则

async是普通的JavaScript函数,它们有如下不一样之处--

async 函数老是返回 promise 对象。

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

函数 fn 返回 'hello',因为咱们使用了 async 关键字, 'hello' 被包装成了一个 promise 对象(经过 Promise 构造函数实现)

这是另一种实现方式,不使用 async

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

上面代码中,咱们手动返回了一个 promise 对象,没有使用 async 关键字

更准确地说,async函数的返回值会被 Promise.resolve 包裹。

若是返回值是原始值,Promise.resolve 会返回一个promise化的值,若是返回值是一个promise对象,则直接返回这个对象

// in case of primitive values
const p = Promise.resolve('hello')
p instanceof Promise; 
// true
// p is returned as is it
Promise.resolve(p) === p; 
// true
复制代码

若是async函数中抛出错误怎么办?

好比--

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

若是错误未被捕获,foo() 函数会返回一个状态为 rejectedpromise。不一样于 Promise.resolvePromise.reject 会包裹错误并返回。详情请看稍后的错误处理部分。

最终的结果是,不管你返回什么结果,你都将从async函数中获得一个promise。

async 函数遇到 await <表达式>时会暂停

await做用于一个表达式。当表达式是一个promise时,async函数会暂停执行,直到该promise状态变为 resolved。当表达式为非promise值时,会使用 Promise.resolve 将其转换为promise,而后状态变为 resolved

// 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 = await 9,内部会被解析为 const a = await Promise.resolve(9)
  • 由于使用到了 await, 因此函数执行会暂停,直到变量 a 获得一个值,在promise 会将其resolve为9
  • delayAndGetRandom(1000) 会使 fn 函数暂停,直到1秒钟以后 delayAndGetRandom 被resolve,因此,fn 函数的执行有效地暂停了 1 秒钟
  • 此外,delayAndGetRandom 返回一个随机数。不管在resolve函数中传递什么,它都被分配给变量 b
  • 一样,变量 c 值为 5 ,而后使用 await delayAndGetRandom(1000) 又延时了 1 秒钟。在这行代码中咱们并无使用 Promise.resolve 返回值。
  • 最后,咱们计算 a + b * c 结果,并用 Promise.resolve 包裹并返回

注意:若是这里函数的暂停和恢复使你想起了 ES6 generators ,那是由于 generator 有不少 优势

解决方案

让咱们用 async/await 来解决文章开头提出一个假设问题:

使用 async/await

咱们定义了一个 async 函数 finishMyTask,使用 await 等待 queryDatabase, sendEmail, logTaskInFile 函数执行返回结果

若是咱们将 async/await 解决方案与使用 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函数,更简单

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

错误处理

咱们在语法部分所了解的,一个未捕获的 Error() 会被包装在一个 rejected promise 中,可是,咱们能够在 async 函数中同步地使用 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() 是一个异步函数,要么 resolve 'perfect number',要么 reject Error('Sorry, number too big')

看看下面的代码

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

由于咱们在等待 canRejectOrReturn 执行,它的 rejection 会被转换为一个错误抛出,catch 会执行,也就是说 foo 函数结果要么 resolve 为 undefined(由于咱们在 try 中没有返回值),要么 resolve 'error caught'。由于咱们在 foo 函数中使用了 try-catch 处理错误,因此说 foo 函数的结果永远不会是 rejected。

另外一个例子

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

注意。此次在 foo 函数里面咱们返回而不是等待 canRejectOrReturnfoo 要么 resolve 'perfect number',要么 reject Error('Sorry, number too big')catch 语句不会被执行

由于咱们 returncanRejectOrReturn 返回的 promise 对象,所以 foo 最终的状态由 canRejectOrReturn 的状态决定,你能够将 return canRejectOrReturn() 分红两行代码,来更清楚的了解,注意第一行没有 await

try {
    const promise = canRejectOrReturn();
    return promise;
}
复制代码

让咱们来看看 returnawait 一块儿使用的状况

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

在这种状况下 foo resolve 'perfect number',或者 resolve 'error caught',没有 rejection,就像上面那个只有 await 的例子,在这里咱们 resolve 了 canRejectOrReturn 返回的值,而不是 undefined

比也能够将 return await canRejectOrReturn() 拆分来看

try {
    const value  = await canRejectOrReturn();
    return value;
}
// ...
复制代码

常见的错误和陷阱

因为 Promise 和 async/await 之间错综复杂的操做。 可能会有一些隐藏的错误,咱们来看看 -

没有使用 await

有时候咱们会忘记在 promise 前面使用 await,或者忘记 return

async function foo() {
  try {
    canRejectOrReturn();
  } catch (e) {
    return 'caught';
  }
}
复制代码

注意,若是咱们不使用 await 或者 returnfoo 老是会 resolve undefined,不会等待一秒,可是 canRejectOrReturn() 中的 promise 的确被执行了。若是有反作用,也会产生,若是抛出错误或者 reject,UnhandledPromiseRejectionWarning 就会产生

在回调中使用 async 函数

咱们常常在 .map.filter 中使用 async 函数做为回调函数,假设咱们有一个函数 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;
});
复制代码

注意 async.map 的回调函数中,咱们但愿 counts 变量包含仓库数量,然而就像咱们以前看到,async 函数会返回一个 promise 对象,所以 counts 其实是一个 promises 数组,这个数组包含着每次调用函数获取用户仓库数量返回的 promise,

过分按顺序使用 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 数组中。程序的问题在于第一个用户的 count 被获取以后,第二个用户的 count 才能被获取。同一时间,只能获取一个仓库的数量。

若是一个 fetch 操做耗时 300 ms,那么 fetchAllCounts 函数耗时大概在 900 ms 左右。因而可知,程序耗时会随着用户数量的增长而线性增长。由于获取不一样用户公开仓库数量之间没有依赖关系,因此咱们能够将操做并行处理。

咱们能够同时获取用户,而不是按顺序执行。 咱们将使用 .mapPromise.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 对象做为输出。当全部 promise 对象的状态都转变成 resolved 时,返回值是一个包含全部结果的数组,而失败的时候则返回最早被reject失败状态的值,只要有一个 promise 对象被 rejected,Promise.all 的返回值为第一个被 rejected 的 promise 对象对应的返回值。可是,同时运行全部 promise 可能行不通。若是你想批量完成 promise。能够参考 p-map 关于数量可控的并发操做。

总结

async 函数很是重要。随着 Async Iterators 的引入,async 函数将会应用得愈来愈广。对于现代 JavaScript 开发人员来讲掌握并理解 async 函数相当重要。但愿这篇文章能对你有所帮助

相关文章
相关标签/搜索