详解ES6之async+await 同步/异步方案


Posted in Javascript onSeptember 19, 2017

异步编程一直是JavaScript 编程的重大事项。关于异步方案, ES6 先是出现了 基于状态管理的 Promise,然后出现了 Generator 函数 + co 函数,紧接着又出现了 ES7 的 async + await 方案。

本文力求以最简明的方式来疏通 async + await。

异步编程的几个场景

先从一个常见问题开始:一个for 循环中,如何异步的打印迭代顺序?

我们很容易想到用闭包,或者 ES6 规定的 let 块级作用域来回答这个问题。

for (let val of [1, 2, 3, 4]) {
  setTimeout(() => console.log(val),100);
}
// => 预期结果依次为:1, 2, 3, 4

这里描述的是一个均匀发生的的异步,它们被依次按既定的顺序排在异步队列中等待执行。

如果异步不是均匀发生的,那么它们被注册在异步队列中的顺序就是乱序的。

for (let val of [1, 2, 3, 4]) {
  setTimeout(() => console.log(val), 100 * Math.random());
}
// => 实际结果是随机的,依次为:4, 2, 3, 1

返回的结果是乱序不可控的,这本来就是最为真实的异步。但另一种情况是,在循环中,如果希望前一个异步执行完毕、后一个异步再执行,该怎么办?

for (let val of ['a', 'b', 'c', 'd']) {
  // a 执行完后,进入下一个循环
  // 执行 b,依此类推
}

这不就是多个异步 “串行” 吗!

在回调 callback 嵌套异步操作、再回调的方式,不就解决了这个问题!或者,使用 Promise + then() 层层嵌套同样也能解决问题。但是,如果硬是要将这种嵌套的方式写在循环中,还恐怕还需费一番周折。试问,有更好的办法吗?

异步同步化方案

试想,如果要去将一批数据发送到服务器,只有前一批发送成功(即服务器返回成功的响应),才开始下一批数据的发送,否则终止发送。这就是一个典型的 “for 循环中存在相互依赖的异步操作” 的例子。

明显,这种 “串行” 的异步,实质上可以当成同步。它和乱序的异步比较起来,花费了更多的时间。按理说,我们希望程序异步执行,就是为了 “跳过” 阻塞,较少时间花销。但与之相反的是,如果需要一系列的异步 “串行”,我们应该怎样很好的进行编程?

对于这个 “串行” 异步,有了 ES6 就非常容易的解决了这个问题。

async function task () {
  for (let val of [1, 2, 3, 4]) {
    // await 是要等待响应的
    let result = await send(val);
    if (!result) {
      break;
    }
  }
}
task();

从字面上看,就是本次循环,等有了结果,再进行下一次循环。因此,循环每执行一次就会被暂停(“卡住”)一次,直到循环结束。这种编码实现,很好的消除了层层嵌套的 “回调地狱” 问题,降低了认知难度。

这就是异步问题同步化的方案。关于这个方案,如果说 Promise 主要解决的是异步回调问题,那么 async + await 主要解决的就是将异步问题同步化,降低异步编程的认知负担。

async + await “外异内同”

早先接触这套 API 时,看着繁琐的文档,一知半解的认为 async + await 主要用来解决异步问题同步化的。

其实不然。从上面的例子看到:async 关键字声明了一个 异步函数,这个 异步函数 体内有一行 await 语句,它告示了该行为同步执行,并且与上下相邻的代码是依次逐行执行的。

将这个形式化的东西再翻译一下,就是:

1、async 函数执行后,总是返回了一个 promise 对象
2、await 所在的那一行语句是同步的

其中,1 说明了从外部看,task 方法执行后返回一个 Promise 对象,正因为它返回的是 Promise,所以可以理解task 是一个异步方法。毫无疑问它是这样用的:

task().then((val) => {alert(val)})
   .then((val) => {alert(val)})

2 说明了在 task 函数内部,异步已经被 “削” 成了同步。整个就是一个执行稍微耗时的函数而已。

综合 1、2,从形式上看,就是 “task 整体是一个异步函数,内部整个是同步的”,简称“外异内同”。

整体是一个异步函数 不难理解。在实现上,我们不妨逆向一下,语言层面让async关键字调用时,在函数执行的末尾强制增加一个promise 反回:

async fn () {
  let result;
  // ...
  //末尾返回 promise
  return isPromise(result)? 
      result : Promise.resolve(undefined);
}

内部是同步的 是怎么做到的?实际上 await 调用,是让后边的语句(函数)做了一个递归执行,直到获取到结果并使其 状态 变更,才会 resolve 掉,而只有 resolve 掉,await 那一行代码才算执行完,才继续往下一行执行。所以,尽管外部是一个大大的 for 循环,但是整个 for 循环是依次串行的。

因此,仅从上述框架的外观出发,就不难理解 async + await 的意义。使用起来也就这么简单,反而 Promise 是一个必须掌握的基础件。

秉承本次《重读 ES6》系列的原则,不过多追求理解细节和具体实现过程。我们继续巩固一下这个 “形式化” 的理解。

async + await 的进一步理解

有这样的一个异步操作 longTimeTask,已经用 Promise 进行了包装。借助该函数进行一系列验证。

const longTimeTask = function (time) {
 return new Promise((resolve, reject) => {
  setTimeout(()=>{
   console.log(`等了 ${time||'xx'} 年,终于回信了`);
   resolve({'msg': 'task done'});
  }, time||1000)
 })
}

async 函数的执行情况

如果,想查看 async exec1 函数的返回结果,以及 await 命令的执行结果:

const exec1 = async function () {
 let result = await longTimeTask();
 console.log('result after long time ===>', result);
}
// 查看函数内部执行顺序
exec1();
// => 等了 xx 年,终于回信了
// => result after long time ===> Object {msg: "task done"}

//查看函数总体返回值
console.log(exec1());
// => Promise {[[PromiseStatus]]: "pending",...}
// => 同上

以上 2 步执行,清晰的证明了 exec1 函数体内是同步、逐行逐行执行的,即先执行完异步操作,然后进行 console.log() 打印。而 exec1() 的执行结果就直接是一个 Promise,因为它最先会蹦出来一串 Promise ...,然后才是 exec1 函数的内部执行日志。

因此,所有验证,完全符合 整体是一个异步函数,内部整个是同步的 的总结。

await 如何执行其后语句?

回到 await ,看看它是如何执行其后边的语句的。假设:让 longTimeTask() 后边直接带 then() 回调,分两种情况:

1)then() 中不再返回任何东西
2) then() 中继续手动返回另一个 promise

const exec2 = async function () {
 let result = await longTimeTask().then((res) => {
  console.log('then ===>', res.msg);
  res.msg = `${res.msg} then refrash message`;
  // 注释掉这条 return 或 手动返回一个 promise
  return Promise.resolve(res);
 });
 console.log('result after await ===>', result.msg);
}
exec2();
// => 情况一 TypeError: Cannot read property 'msg' of undefined
// => 情况二 正常

首先,longTimeTask() 加上再多得 then() 回调,也不过是放在了它的回调列队 queue 里了。也就是说,await 命令之后始终是一条 表达式语句,只不过上述代码书写方式比较让人迷惑。(比较好的实践建议是,将 longTimeTask 方法身后的 then() 移入 longTimeTask 函数体封装起来)

其次,手动返回另一个 promise 和什么也不返回,关系到 longTimeTask() 方法最终 resolve 出去的内容不一样。换句话说,await 命令会提取其后边的promise 的 resolve 结果,进而直接导致 result 的不同。

值得强调的是,await 命令只认 resolve 结果,对 reject 结果报错。不妨用以下的 return 语句替换上述 return 进行验证。

return Promise.reject(res);

最后

其实,关于异步编程还有很多可以梳理的,比如跨模块的异步编程、异步的单元测试、异步的错误处理以及什么是好的实践。All in all, 限于篇幅,不在此汇总了。最后,async + await 确实是一个很优雅的方案。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript生成json数据简单示例分享
Feb 14 Javascript
JavaScript截取字符串的Slice、Substring、Substr函数详解和比较
Mar 20 Javascript
jquery手风琴特效插件
Feb 04 Javascript
深入理解JavaScript系列(25):设计模式之单例模式详解
Mar 03 Javascript
jquery实现兼容IE8的异步上传文件
Jun 15 Javascript
完美的js div拖拽实例代码
Sep 24 Javascript
关于vue.js发布后路径引用的问题解决
Aug 15 Javascript
浅谈vue引入css,less遇到的坑和解决方法
Jan 20 Javascript
在vue中封装可复用的组件方法
Mar 01 Javascript
动态加载JavaScript文件的3种方式
May 05 Javascript
Vue结合后台导入导出Excel问题详解
Feb 19 Javascript
微信小程序中限制激励式视频广告位显示次数(实现思路)
Dec 06 Javascript
jQuery实现倒计时功能 jQuery实现计时器功能
Sep 19 #jQuery
jquery实现倒计时小应用
Sep 19 #jQuery
Angular客户端请求Rest服务跨域问题的解决方法
Sep 19 #Javascript
jQuery实现可兼容IE6的遮罩功能详解
Sep 19 #jQuery
jQuery选择器之表单元素选择器详解
Sep 19 #jQuery
JS实现静态页面搜索并高亮显示功能完整示例
Sep 19 #Javascript
原生javascript实现的全屏滚动功能示例
Sep 19 #Javascript
You might like
用PHP实现 上一篇、下一篇的代码
2012/09/29 PHP
php使用ffmpeg向视频中添加文字字幕的实现方法
2016/05/23 PHP
js根据日期判断星座的示例代码
2014/01/23 Javascript
js操纵dom生成下拉列表框的方法
2014/02/24 Javascript
使用jQuery判断IE浏览器版本的代码
2014/06/14 Javascript
轻松创建nodejs服务器(4):路由
2014/12/18 NodeJs
javascript函数式编程程序员的工具集
2015/10/11 Javascript
jquery遍历函数siblings()用法实例
2015/12/24 Javascript
运用js教你轻松制作html音乐播放器
2020/04/17 Javascript
IE8利用自带的setCapture和releaseCapture解决iframe的拖拽事件方法
2016/10/25 Javascript
ionic 3.0+ 项目搭建运行环境的教程
2017/08/09 Javascript
vue嵌套路由与404重定向实现方法分析
2018/05/04 Javascript
新年快乐! javascript实现超级炫酷的3D烟花特效
2019/01/30 Javascript
python操作MySQL数据库具体方法
2013/10/28 Python
Python实现文件复制删除
2016/04/19 Python
Python selenium文件上传方法汇总
2020/11/19 Python
Python中defaultdict与lambda表达式用法实例小结
2018/04/09 Python
对python中raw_input()和input()的用法详解
2018/04/22 Python
python实现Flappy Bird源码
2018/12/24 Python
python机器人运动范围问题的解答
2019/04/29 Python
Django框架用户注销功能实现方法分析
2019/05/28 Python
nginx黑名单和django限速,最简单的防恶意请求方法分享
2019/08/09 Python
Django中create和save方法的不同
2019/08/13 Python
opencv转换颜色空间更改图片背景
2019/08/20 Python
python matplotlib中的subplot函数使用详解
2020/01/19 Python
python构造IP报文实例
2020/05/05 Python
python selenium 获取接口数据的实现
2020/12/07 Python
购买美国制造的相框和画框架:Picture Frames
2018/08/14 全球购物
SIXPAD智能健身仪英国官网:革命性的训练装备品牌
2018/09/27 全球购物
Glamest意大利:女性在线奢侈品零售店
2019/04/28 全球购物
您在慕尼黑的跑步商店:Lauf-bar
2019/10/11 全球购物
法国滑雪假期的专家:Ski Planet
2019/11/02 全球购物
会计专业自我鉴定
2014/02/10 职场文书
车间统计员岗位职责
2015/04/14 职场文书
课题研究阶段性总结
2015/08/13 职场文书
求职信如何撰写?
2019/05/22 职场文书