# async 函数

JS是一门单线程的语言,但是JS中的异步编程的场景太常见了,async函数作为JS异步编程解决方案的王炸,对于async函数大家都想必都比较熟悉。

async函数是Generator的语法糖,但是Generator又是ES6中最难的语法,本文重点不在于阐述async函数的使用,而专注于其底层的运行原理,我们将从babel编译的结果来研究async函数的原理,希望您在阅读本文之前,已掌握Generator底层的运行原理。

# 1、基本使用

async函数的返回结果是一个Promise,也就是说,不管你包不包装这个对象为Promise,它都将给你包装成Promise,比如:

async function func() {
  await 1;
  return Promise.resolve(true);
}

和以下代码是等价的:

async function func() {
  await 1;
  return true;
}

比如,我们可以使用async函数实现一个sleep函数。

function sleep(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
  });
}

async function eventHandler() {
  // 等待1S,继续后续的业务逻辑
  await sleep(1000);
  console.log("执行一些逻辑");
}

await后面的代码是微任务执行的,而await之前的代码是在同步任务执行的(包括这行)。

比如有以下代码:

async function func() {
  console.log(1);
  await something();
  console.log(2);
}

等价于:

function func() {
  new Promise(() => {
    console.log(1);
    return something();
  }).then(() => {
    console.log(2);
  });
}

下面是一道JS事件循环常考的面试题:

//请写出输出内容
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2");
}

console.log("script start");

setTimeout(function () {
  console.log("setTimeout");
}, 0);

async1();

new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("promise2");
});
console.log("script end");

/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

另外,对于await表达式,其实表示的含义就是如果await后面跟随的表达式的如果是一个Promise的话,则取出Promise的值,否则直接取其原值。

# 2、使用的注意点

最好把await命令放在try...catch代码块中,因为一旦一个await表达式发生错误,后续的流程就不会再执行了,这点我们一会儿可以在babel编译的结果中看到为什么。

然后,async函数是不能够使用new调用的,为什么呢?从现象上来说,因为async函数没有prototype属性,从原理上来讲的话,async函数是基于Generator的,Generator函数是不能被用作new调用的,因为async函数当然也不能被用作new调用了。

另外一个比较重要的注意点是,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

比如下述代码:

async function func() {
  let foo = await getFoo();
  let bar = await getBar();
}

需要首先执行getFoogetFoo变成fulfilled之后,才会再执行getBar。在有些时候,两个操作流程并没有先后顺序的要求,对于用户而已,更快的数据到达是较好的选择,因此,这种场景下,需要写成以下形式:

// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

上述的写法1大家可能比较好理解,但是写法2可能就会比较迷惑了,这样理解就行了,首先,我们没有通过用await关键字来调用getFoogetBar,也就是说,在同一时刻,这两个操作就已经执行了,但是,只不过要必须fooPromise的状态为fulfilled才能执行取barPromise状态的逻辑,而实际上两者并没有先后顺序,请读者体会其中的区别。

如何判断一个函数是否是async函数呢,其实方法非常简单:

// 方法一
const isAsyncFunc = (val) => {
  return val && val[Symbol.toStringTag] === "AsyncFunction";
};

// 方法二
const isAsyncFunc = (func) => {
  return (
    typeof func === "function" &&
    Object.prototype.toString.call(func) === "[object AsyncFunction]"
  );
};

明显感觉方法一要优雅很多,对于这个用法不太清楚的同学可以参考Symbol相关的知识点。

# 3、async 函数的实现原理

对于以下代码,我们看看babel将会把它编译成什么样子

async function func() {
  const val1 = await 1;
  const val2 = await (2 + val1);
  const val3 = await (3 + val2);
  const val4 = await (4 + val3);
  return val4;
}

babel编译之后的结果(为了篇幅,已经省略了generator-runtime的部分):

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
  try {
    var info = gen[key](arg);
    var value = info.value;
  } catch (error) {
    // 如果执行出错,提前结束
    reject(error);
    return;
  }
  // 如果Generator已经迭代完成,直接把最终的返回值报告给外部的Promise,作为它的fulfilled值,结束递归
  if (info.done) {
    resolve(value);
  } else {
    // 没有完成,把本轮的值包裹,最为入参传递给下一个next或者throw的调用
    Promise.resolve(value).then(_next, _throw);
  }
}

function _asyncToGenerator(fn) {
  // fn就是一个Generator,执行它可以得到一个迭代器
  return function () {
    var self = this,
      args = arguments;
    return new Promise(function (resolve, reject) {
      // 得到一个由Generator执行得到的迭代器
      var gen = fn.apply(self, args);
      // 定义next函数
      function _next(value) {
        // 递归的调用next,以使得Generator执行得到的迭代器可以一直向后迭代
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
      }
      // 定义错误处理函数
      function _throw(err) {
        // 递归的调用throw,以使得Generator执行得到的迭代器可以一直向后迭代
        asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
      }
      // 开始迭代,因为第一个next不能有参数,所以就传递了一个undefined
      _next(undefined);
    });
  };
}

function func() {
  return _func.apply(this, arguments);
}

function _func() {
  _func = _asyncToGenerator(
    // 得到一个Generator,这个Generator执行就可以得到一个迭代器
    /*#__PURE__*/ _regeneratorRuntime().mark(function _callee() {
      var val1, val2, val3, val4;
      return _regeneratorRuntime().wrap(function _callee$(_context) {
        while (1) {
          switch ((_context.prev = _context.next)) {
            case 0:
              _context.next = 2;
              return 1;

            case 2:
              val1 = _context.sent;
              _context.next = 5;
              return 2 + val1;

            case 5:
              val2 = _context.sent;
              _context.next = 8;
              return 3 + val2;

            case 8:
              val3 = _context.sent;
              _context.next = 11;
              return 4 + val3;

            case 11:
              val4 = _context.sent;
              return _context.abrupt("return", val4);

            case 13:
            case "end":
              return _context.stop();
          }
        }
      }, _callee);
    })
  );
  // 向外界返回一个Promise
  return _func.apply(this, arguments);
}

async函数的本质是Generator,但是Generator的使用是相当麻烦的,你得运行它,然后不断的调用next直到迭代完成,而async函数直接就在内部帮我们把这个事儿做了,你就只需要专注于业务代码就好了。

首先分析asyncGeneratorStep这个函数,当所有的await语句都走完了之后,返回状态为fulfilled结果,如果中途产生错误则提前终止了,这就是为什么前文提到因为一旦await后面的表达式发生错误,后续的流程就不会再执行了的原因。如果还没有将当前的迭代器迭代完成,则继续向后迭代,为什么是这样的呢?

function _next(value) {
  asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}

因为在这个位置,_next函数被用作了参数传递到asyncGeneratorStep,这儿是一个递归调用,而递归的退出条件呢?要么执行完,要么出现错误。

然后,可以看到_asyncToGenerator这个函数返回值是个匿名函数,并且这个匿名函数的返回值是一个Promise,这也就应证了前文所说的,在async函数中没有必要刻意的对返回值进行Promise.resolve这样的操作。

最后,为什么在第一次调用_next函数的时候传递的是undefined,因为Generator节,我们讲过,其在第一个next方法的时候是无法传递参数的。

另外,这个是在阮一峰老师ES6入门书籍里面他给出的spawn函数的实现。

function spawn(genF) {
  return new Promise(function (resolve, reject) {
    const gen = genF();
    function step(nextF) {
      let next;
      try {
        next = nextF();
      } catch (e) {
        return reject(e);
      }
      if (next.done) {
        return resolve(next.value);
      }
      Promise.resolve(next.value).then(
        function (v) {
          step(function () {
            return gen.next(v);
          });
        },
        function (e) {
          step(function () {
            return gen.throw(e);
          });
        }
      );
    }
    step(function () {
      return gen.next(undefined);
    });
  });
}

上述实现嵌套的层级较多,意思都是一个,但是从可读性来说,我个人感觉不如babel编译之后的代码,不喜勿喷。