# 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();
}
需要首先执行getFoo等getFoo变成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关键字来调用getFoo和getBar,也就是说,在同一时刻,这两个操作就已经执行了,但是,只不过要必须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编译之后的代码,不喜勿喷。