详解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 相关文章推荐
基于jquery+thickbox仿校内登录注册框
Jun 07 Javascript
jquery实现table鼠标经过变色代码
Sep 25 Javascript
解决window.opener=null;window.close(),只支持IE6不支持IE7,IE8的问题
Jan 14 Javascript
利用JavaScript判断浏览器类型及版本
Aug 23 Javascript
原生Javascript插件开发实践
Jan 18 Javascript
tablesorter.js表格排序使用方法(支持中文排序)
Feb 10 Javascript
使用jquery datatable和bootsrap创建表格实例代码
Mar 17 Javascript
JavaScript实现的斑马线表格效果【隔行变色】
Sep 18 Javascript
详解KOA2如何手写中间件(装饰器模式)
Oct 11 Javascript
详解webpack打包后如何调试的方法步骤
Nov 07 Javascript
详解angular2 控制视图的封装模式
Dec 27 Javascript
javascript实现贪吃蛇小游戏
Jul 28 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 CURL模拟GET及POST函数代码
2010/04/25 PHP
php实现比较全的数据库操作类
2015/06/18 PHP
ThinkPHP 模板substr的截取字符串函数详解
2017/01/09 PHP
Sample script that displays all of the users in a given SQL Server DB
2007/06/16 Javascript
用JavaScript对JSON进行模式匹配(Part 1-设计)
2010/07/17 Javascript
jQuery 图片切换插件(代码比较少)
2012/05/07 Javascript
JS,Jquery获取select,dropdownlist,checkbox下拉列表框的值(示例代码)
2014/01/11 Javascript
JavaScript Array对象扩展indexOf()方法
2014/05/09 Javascript
JavaScript实现的in_array函数
2014/08/27 Javascript
PHP中使用微秒计算脚本执行时间例子
2014/11/19 Javascript
深入分析JSON编码格式提交表单数据
2015/06/25 Javascript
深入分析jsonp协议原理
2015/09/26 Javascript
浅谈Angular的$q, defer, promise
2016/12/20 Javascript
js实现时间轴自动排列效果
2017/03/09 Javascript
浅谈Vuejs Prop基本用法
2017/08/17 Javascript
jQuery 实现左右两侧菜单添加、移除功能
2018/01/02 jQuery
JS实现将二维数组转为json格式字符串操作示例
2018/07/12 Javascript
微信小程序wx.request的简单封装
2019/11/13 Javascript
[01:05:40]VG vs Newbee 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/20 DOTA
python操作日期和时间的方法
2014/03/11 Python
Python中装饰器的一个妙用
2015/02/08 Python
Python环境搭建之OpenCV的步骤方法
2017/10/20 Python
Python 3.x基于Xml数据的Http请求方法
2018/12/28 Python
Python实现简单查找最长子串功能示例
2019/02/26 Python
python实现文件的备份流程详解
2019/06/18 Python
使用Filter过滤python中的日志输出的实现方法
2019/07/17 Python
python被修饰的函数消失问题解决(基于wraps函数)
2019/11/04 Python
Python 通过监听端口实现唯一脚本运行方式
2020/05/05 Python
css3 中translate和transition的使用方法
2020/03/26 HTML / CSS
HTML5新增的标签和属性归纳总结
2018/05/02 HTML / CSS
英国最大的宠物食品和宠物用品网上零售商: Zooplus
2016/08/01 全球购物
不忘国耻振兴中华演讲稿
2014/05/14 职场文书
个人房屋买卖协议书(范本)
2014/10/04 职场文书
法人单位授权委托书范文
2014/10/06 职场文书
大学生考试作弊检讨书1000字
2014/10/14 职场文书
再也不用花钱买漫画!Python爬取某漫画的脚本及源码
2021/06/09 Python