详解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 相关文章推荐
让GoogleCode的SVN下的HTML文件在FireFox下正常显示.
May 25 Javascript
jquery each()源代码
Feb 14 Javascript
Javascript获取CSS伪元素属性的实现代码
Sep 28 Javascript
jquery中页面Ajax方法$.load的功能使用介绍
Oct 20 Javascript
jQuery实现点击图片翻页展示效果的方法
Feb 16 Javascript
JavaScript显示当前文档最后修改日期的方法
Mar 19 Javascript
jQuery插件formValidator自定义函数扩展功能实例详解
Nov 25 Javascript
javascript实现checkbox复选框实例代码
Jan 10 Javascript
BootStrap 智能表单实战系列(二)BootStrap支持的类型简介
Jun 13 Javascript
BootStrap fileinput.js文件上传组件实例代码
Feb 20 Javascript
jquery传参及获取方式(两种方式)
Feb 13 jQuery
vue实现移动端触屏拖拽功能
Aug 21 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和ACCESS写聊天室(五)
2006/10/09 PHP
PHP6新特性分析
2016/03/03 PHP
Thinkphp5 微信公众号token验证不成功的原因及解决方法
2017/11/12 PHP
JavaScript 中的事件教程
2007/04/05 Javascript
jQuery 处理网页内容的实现代码
2010/02/15 Javascript
JS返回上一页实例代码通过图片和按钮分别实现
2013/08/16 Javascript
js函数调用的方式
2014/05/06 Javascript
多引号嵌套的变量命名的问题
2014/05/09 Javascript
javascript省市级联功能实现方法实例详解
2015/10/20 Javascript
JavaScript生成二维码图片小结
2015/12/27 Javascript
jQuery插件HighCharts绘制2D金字塔图效果示例【附demo源码下载】
2017/03/09 Javascript
原生JS实现左右箭头选择日期实例代码
2017/03/14 Javascript
ES6扩展运算符用法实例分析
2017/10/31 Javascript
Angular实现表单验证功能
2017/11/13 Javascript
关于Google发布的JavaScript代码规范你要知道哪些
2018/04/04 Javascript
vue首次赋值不触发watch的解决方法
2018/09/11 Javascript
vue-cli的工程模板与构建工具详解
2018/09/27 Javascript
如何基于JS截获动态代码
2019/12/25 Javascript
python列表操作使用示例分享
2014/02/21 Python
Python中join和split用法实例
2015/04/14 Python
Python中的并发处理之asyncio包使用的详解
2018/04/03 Python
python字符串常用方法
2018/06/14 Python
详解python运行三种方式
2019/05/13 Python
Django REST框架创建一个简单的Api实例讲解
2019/11/05 Python
python不相等的两个字符串的 if 条件判断为True详解
2020/03/12 Python
python实现发送QQ邮件(可加附件)
2020/12/23 Python
一文读懂python Scrapy爬虫框架
2021/02/24 Python
python SOCKET编程基础入门
2021/02/27 Python
美国紧身牛仔裤品牌:NYDJ
2017/05/24 全球购物
广州某公司软件工程师面试题
2014/12/22 面试题
房地产开发计划书
2014/01/10 职场文书
八一建军节演讲稿
2014/09/10 职场文书
查摆问题整改措施
2014/10/24 职场文书
导游词欢迎词
2015/02/02 职场文书
《7的乘法口诀》教学反思
2016/02/18 职场文书
MongoDB 常用的crud操作语句
2021/06/20 MongoDB