ES6 中的 Async 函数
async 函数是 Generator 函数的语法糖,它将 Generator 函数的 星号*
替换成 async
,将 yield
替换成 await
async 函数对 Generator 函数的改进:
- 内置执行器
- 更好的语义
- 更广的适用性
co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
- 返回值是 Promise
async 函数的返回值是 Promise 对象 ,这比 Generator 函数的返回值是 Iterator 对象 方便多了。你可以用 then 方法指定下一步的操作。
进一步说,async 函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖
基本用法
// 指定 50 毫秒以后,输出 hello world
function timeout(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
- async 函数返回一个 Promise 对象,内部 return 语句返回的值,会成为 then 方法回调函数的参数。async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态 。抛出的错误对象会被 catch 方法回调函数接收到。
await 命令后面的 Promise 对象如果变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到
- async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误 。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数
- 正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
另一种情况是,await 命令后面是一个 thenable 对象(即定义 then 方法的对象),那么 await 会将其等同于 Promise 对象。
- 借助 await 命令就可以让程序停顿指定的时间
function sleep(interval) { return new Promise(resolve => { setTimeout(resolve, interval); }) } // 用法 async function one2FiveInAsync() { for(let i = 1; i <= 5; i++) { console.log(i); await sleep(1000); } } one2FiveInAsync();
- 任何一个 await 语句后面的 Promise 对象变为 reject 状态,那么整个 async 函数都会中断执行 。
如果希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个 await 放在 try...catch 结构里面 ,这样不管这个异步操作是否成功,第二个 await 都会执行。
另一种方法是 await 后面的 Promise 对象再跟一个 catch 方法 ,处理前面可能出现的错误
- 多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发
- await 命令只能用在 async 函数之中,如果用在普通函数,就会报错
async function dbFuc(db) { let docs = [{}, {}, {}]; // 报错 docs.forEach(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 方法 // 下面 reduce 方法的第一个参数是 async 函数,导致该函数的第一个参数是前一步操作返回的 Promise 对象,所以必须使用 await 等待它操作结束。另外,reduce 方法返回的是 docs 数组最后一个成员的 async 函数的执行结果,也是一个 Promise 对象,导致在它前面也必须加上 await async function dbFuc(db) { let docs = [{}, {}, {}]; await docs.reduce(async (_, doc) => { await _; await db.post(doc); }, undefined); }
- 希望多个请求并发执行,可以使用
Promise.all
方法。当三个请求都会 resolved 时,下面两种写法效果相同async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results); } // 或者使用下面的写法 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); }
- async 函数可以保留运行堆栈
const a = () => { b().then(() => c()); };
上面代码中,函数 a 内部运行了一个异步任务 b()。当 b() 运行的时候,函数 a() 不会中断,而是继续执行。等到 b() 运行结束,可能 a() 早就运行结束了,b() 所在的上下文环境已经消失了。如果 b() 或 c() 报错,错误堆栈将不包括 a()。
const a = async () => { await b(); c(); };
上面代码中,b() 运行的时候,a() 是暂停执行,上下文环境都保存着。一旦 b() 或 c() 报错,错误堆栈将包括 a()。
实例:按顺序完成异步操作
依次远程读取一组 URL,然后按照读取的顺序输出结果 :
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,因此实现了按顺序输出。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: ES6 字符串的扩展
下一篇: 彻底找到 Tomcat 启动速度慢的元凶
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论