[ES6] async/await 应用指南

async/await 是什么

async/await 是 ES7 引入的新的异步代码 规范,它提供了一种新的编写异步代码的方式,这种方式在语法层面提供了一种形式上很是接近于同步代码的异步非阻塞代码风格,在此以前咱们使用的可能是异步回调、 Promise 模式。
从实现上来看 async/await 是在 生成器、Promise 基础上构建出来的新语法:以 生成器 实现流程控制,以 Promise 实现异步控制。
Node 自 v8.0.0 起已经彻底支持 async/await 语法,babel 也已经彻底支持 async/await 语法的转译。javascript

下面,咱们以一个一个实例的方式,由浅入深介绍 async/await 语法的使用。java

一个简单的实例

咱们来实现一个获取登陆用户信息的函数,逻辑以下:node

  1. 获取用户登陆态
  2. 若是用户已经登陆,返回对应的用户信息
  3. 若是用户未登陆,跳转到登陆页

以回调方式实现

回调 在最第一版本的 JS 就已经出现,可谓历史悠久,到如今也还保持着至关的活力。
若是以回调方式实现上述需求,代码大概以下:babel

function getProfile(cb) {
  isUserLogined(req.session, (err, isLogined) => {
    if (err) {
      cb(err);
    } else if (isLogined) {
      getUser(req.session, (err, profile) => {
        if (err) {
          cb(err);
        } else {
          cb(null, profile);
        }
      });
    } else {
      cb(null, false);
    }
  });
}

感觉到臭味了吗?这里咱们还只是实现了两层的异步调用,代码中就已经有许多问题,好比重复的 if(err) 语句;好比层层嵌套的函数。
另外,若是在层层回调函数中出现异常,调试起来是很是让人奔溃的 —— 因为 try-catch 没法捕获异步的异常,咱们只能不断不断的写 debugger 去追踪,简直步步惊心。
这种层层嵌套致使的代码臭味,被称为 回调地狱,在过去是困惑社区的一个大问题。session

以 Promise 方式实现

Promise 模式最先只是社区出现的一套解决方案,但凭借其优雅的链式调用语句,获得愈来愈多人的青睐,最终被列为 ES6 的正式规范。
上面的需求,若是以 Promise 模式实现:异步

function getProfile() {
  return isUserLogined(req.session)
    .then(isLogined => {
      if (isLogined) {
        return getUser(req.session);
      }
      return false;
    })
    .catch(err => {
      console.log(err);
    });
}

ok,这减小了些模板代码,也有了一致的异常 catch 方案。但这里面也有其余的一些坑,好比,若是咱们要 resolve 两个不一样 Promise 的值?假设上面的例子中,咱们还须要返回用户的日志记录:async

function getProfile() {
  return isUserLogined(req.session)
    .then(isLogined => {
      if (isLogined) {
        return getUser(req.session).then(profile => {
          return getLog(profile).then(logs => Promise.resolve(profile, logs));
        });
      }
      return false;
    })
    .catch(err => {
      console.log(err);
    });
}

上面的代码在 getUser.then 中嵌套了一层 getLog.then ,这在代码上破坏了 Promise 的链式调用法则,并且,getUser.then 函数中发生的异常是没法被外层的 catch 函数捕获的,这破坏了异常处理的一致性。函数

Promise 的另外一个问题,是在 catch 函数中的异常堆栈不够完整,致使难以追寻真正发生错误的位置。好比如下代码中:oop

function asyncCall(){
    return asyncFunc()
      .then(()=>asyncFunc())
      .then(()=>asyncFunc())
      .then(()=>asyncFunc())
      .then(()=>throw new Error('oops'));
}

asyncCall()
  .catch((e)=>{
    console.log(e);
    // 输出:
    // Error: oops↵    at asyncFunc.then.then.then.then (<anonymous>:6:22)
  });

因为抛出异常的语句是在一个匿名函数中,运行时会认为错误发生的位置是 asyncFunc.then.then.then.then,假如代码中大量使用了 asyncFunc 函数,那么上面的报错信息就很难帮助咱们准肯定位错误发生的位置。
咱们固然能够给每一个 then 的回调函数赋予一个有意义的名词,但这又丧失了箭头函数、匿名函数的简洁。性能

以 async/await 方式实现

最后,终于轮到咱们此次的主题 —— async/await 方式的异步代码,虽然这是一个 ES7 规范,但配合强大的 babel,如今已经能够大胆使用。
以上需求的实现代码:

async function getProfile() {
  const isLogined = await isUserLogined(req.session);
  if (isLogined) {
    return await getUser(req.session);
  }
  return false;
}

代码比上面两种风格要简单了许多,形式上就是同步操做流程,与咱们的需求描述也很是很是的接近。

async 关键字用于声明一个函数是异步的,能够出如今任何函数声明语句中,包括:普通函数、箭头函数、类函数。普通函数的 constructorFunction, 而被 async 关键字修饰的函数则是 AsyncFunction 类型的:

Object.getPrototypeOf(function() {}).constructor;
// output
// Function() { [native code] }

Object.getPrototypeOf(async function() {}).constructor;
// output
// AsyncFunction() { [native code] }

await 关键字只能在 async 函数中使用,用于声明一个异步调用,好比上面例子中的 const isLogined = await isUserLogined(req.session);,当 async 风格的 getProfile 函数执行到该语句时,会挂起当前函数,将后续语句加入到 event loop 循环中,这一点与 生成器 执行特性相同。
直到 isUserLogined 函数 resovle 后,才继续执行后面的语句。

咱们能够在 async 函数中编写任意数量的 await 语句,async 函数的执行会一直处在 执行-挂起-执行 的循环中,这种特性获得了语言层面的支持,并不须要咱们为此编写多余的代码,这就为复杂的异步场景提供便捷的实现方案,好比:

async function asyncCall() {
  const v1 = await asyncFunc();
  const v2 = await asyncFunc(v1);
  const v3 = await asyncFunc(v2);
  return v3;
}

到这里,咱们已经简单了解了 async/await 的用法,这种同步风格的异步处理方案,相比而言会更容易维护。

async 中的异常处理

上面咱们提到,在 Promise 模式中,catch 函数难以得到完整的异常信息,致使在 Promise 下作调试变得困难重重,那在 async/await 中呢?
咱们来看一段代码:

async function asyncCall() {
  try {
    await asyncFunc();
    throw new Error("oops");
  } catch (e) {
    console.log(e);
    // output
    // Error: oops  at asyncCall (<anonymous>:4:11)
  }
}

相比 Promise 模式,上面代码中异常发生的位置是 asyncCall 函数!相对而言,容易定位了许多。

并联的 await

async/await 语法确实很简单好用,但却容易用岔了。如下面代码为例:

async function retriveProfile(email) {
  const user = await getUser(email);
  const roles = await getRoles(user);
  const level = await getLevel(user);
  return [user, roles, level];
}

上面代码实现了获取用户基本信息,而后经过基本信息获取用户角色、级别信息的功能,其中 getRolesgetLevel 二者之间并没有依赖,是两个并联的异步操做。
但代码中 getLevel 却须要等待 getRoles resolve 以后才能执行。并非全部人都会犯这种错误,而是同步风格很容易诱惑咱们忽略掉真正的异步调用次序,而陷入过于简化的同步思惟中。写这一段的目的正是为了警醒你们,async 只是形式上的同步,根本上仍是异步的,请注意不要让使用者把时间浪费在无谓的等待上。
上面的逻辑,用一种稍微 一些的方式来实现,就能够避免这种性能损耗:

async function retriveProfile(email) {
    const user = await getUser(email);
    const p1 = getRoles(user);
    const p2 = getLevel(user);
    const [roles, levels] = await Promise.all(p1, p2);
    return [user, roles, levels];
}

注意,代码中的 getRolesgetLevel 函数都没有跟在 await 关键字以后,而是把函数返回的 Promise 存放在变量 p1p2 中,后续才对 p1p2 执行 await 声明, getRolesgetLevel 就能同时执行,不需等待另外一方的完成。

这个问题在循环场景下特别容易发生,假设咱们须要获取一批图片的大小信息:

async function retriveSize(imgs) {
  const result = [];
  for (const img of imgs) {
    result.push(await getSize(img));
  }
}

代码中的每次 getSize 调用都须要等待上一次调用完成,一样是一种性能浪费。一样的功能,用这样的方式会更合适:

async function retriveSize(imgs) {
  return Promise.all(imgs.map(img => getSize(img)));
}

这实际上已经回退到了 Promise 模式,因此为了写出良好的 async/await 代码,建议仍是认真学习学习 Promise 模式

相关文章
相关标签/搜索