es2017的async
Last Updated:2022-10-31
async是Generator的语法糖
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。 async 函数是什么?一句话,它就是 Generator 函数的语法糖。
并发执行多个异步操作
两种方式
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
数组需要继发执行(串行、非并行)的几个方法
可使用for...of
,而不是forEach
遍历
function dbFuc(db) { //这里不需要 async
let docs = [{}, {}, {}];
// 可能得到错误结果
docs.forEach(async function (doc) {
await db.post(doc);
});
}
上面代码可能不会正常工作,原因是这时三个db.post()
操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for
循环。
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
另一种是使用数组的reduce
方法
async function dbFuc(db) {
let docs = [{}, {}, {}];
await docs.reduce(async (_, doc) => {
await _;
await db.post(doc);
}, undefined);
}
上面的reduce()
的参数函数里面没有return
语句,原因是这个函数的主要目的是db.post()
操作,不是返回值。而且async
函数不管有没有return
语句,总是返回一个 Promise 对象,所以这里的return
是不必要的。
async 函数可以保留运行堆栈
const a = () => {
b().then(() => c());
};
上面代码中,函数a
内部运行了一个异步任务b()
。当b()
运行的时候,函数a()
不会中断,而是继续执行。等到b()
运行结束,可能a()
早就运行结束了,b()
所在的上下文环境已经消失了。如果b()
或c()
报错,错误堆栈将不包括a()
。
现在将这个例子改成async
函数。
const a = async () => {
await b();
c();
};
上面代码中,b()
运行的时候,a()
是暂停执行,上下文环境都保存着。一旦b()
或c()
报错,错误堆栈将包括a()
。
async 函数的实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
所有的async
函数都可以写成上面的第二种形式,其中的spawn
函数就是自动执行器。
下面给出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); });
});
}
并行发起异步操作,但是操作结果需要继发(串行)
例如,并行发起一系列ajax操作,然后按照读取的顺序输出结果(继发、串行)操作。
有缺陷的(不符合要求的)
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
上面代码问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。
符合要求的
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
上面代码中,虽然map
方法的参数是async
函数,但它是并发执行的,因为只有async
函数内部是继发执行,外部不受影响。后面的for..of
循环内部使用了await
,因此实现了按顺序输出。
这一写法和Promise.all
区别
Promise.all
有一个失败,那么全部失败。
而上述写法,虽然目前和Promise.all
是一样的。但是比较容易改写(try...catch
)做到,就算有一个失败,也可以把之后的都执行掉
// 按次序输出
for (const textPromise of textPromises) {
try{
console.log(await textPromise)
} catch(e) {
console.error(`有promise失败`)
}
}
es2022引入的'模块顶层使用await'命令
只能用户es6
模块,不能用于commonjs
模块
注意,顶层await
只能用在 ES6 模块,不能用在 CommonJS 模块。这是因为 CommonJS 模块的require()
是同步加载,如果有顶层await
,就没法处理加载了。