由于Javascript是一个单线程语言,大量的API都是异步实现的。异步代码有一个很讨厌的问题,会传染。当你在一个函数中使用一个异步API时,你需要通过回调执行后续的逻辑,而当外层逻辑使用这个函数并且依赖后续的逻辑时,你需要继续向外回调,外层函数也需要提供回调函数,于是异步逻辑将一直传染至发起调用者。
举一个例子:

function getDataSync(url) {var req = new XMLHttpRequest();req.open('GET', url, false);req.send();return req.responseText;
}
function getDataAsync(url, callback) {var req = new XMLHttpRequest();req.onreadystatechange = function cb () {callback(req.responseText);};req.open('GET', url, true);req.send();
}
// 同步调用
console.log(getDataSync('data.json'));
// 异步调用
getDataAsync('data.json', function (data) {console.log(data);
}); 

第一个函数getDataSync同步地调用了req.open(),第二个函数getDataAsync则是异步调用,可以看到getDataAsync不仅要定义一个回调函数(就是那个cb)并且要提供一个参数接收外层调用者的回调函数。

异步传染将一直蔓延到最外层逻辑,如果发起调用者本身就是事件,噩梦就终止于此了,但是这意味着程序是靠不同的事件驱动的,这就是为什么说Javascript是事件驱动的模型。但是这样存在一个问题,多个事件的逻辑之间上下文是独立的,或者混杂的。
对于这个例子里的getDataAsync来说, 内部的回调函数可以访问getDataAsync内的上下文(也就是闭包变量req),并且通过回调函数callback把上下文的部分信息(req.responseText)通过参数继续传递给外层调用者。但是如上面所说,在层层调用的尽头,发起者将是一个事件回调。最终这个上下文将丢失在调用栈中,唯一的方法就是在这个过程中或者调用之前就把它保存起来,例如保存在一个全局变量或对象属性上,但是这么做可能把这个上下文信息暴露在一个公共的作用域中。
例如一个涉及前后端通信的某种功能的类,客户端向服务端发送一个消息,然后异步地等待服务端返回消息,通常是一个消息有一个监听事件的回调函数。当消息发送以后,一些上下文信息,或者说状态,保存在类的属性中,在返回消息事件的回调中取得这个属性继续执行后续的逻辑。这个类可能用到多个消息,不仅上下文信息的传递变得繁琐,而且多个消息的不相关逻辑对彼此之间保存的信息都可见,这就像大量使用全局变量一样糟糕。
当然也可以采用上面例子中的写法,在发送请求消息的方法中定义一个匿名函数来监听事件,通过闭包变量保持上下文。但是这种方法不仅写起来不方便,而且在外层调用栈可能仍然要面临这样的问题。还有很重要的一点,函数对象的创建是有开销的,函数越复杂开销越大。
在解决这一问题上,首先Promise被提了出来。先看这样一段代码:

function getDataPromise(url) {var promise = new Promise(function(resolve, reject) {var req = new XMLHttpRequest();req.onreadystatechange = function cb () {resolve(req.responseText); // 注意这里
        };req.open('GET', url, true);req.send();});promise.then(function(value) {console.log(value); });return promise;
}
getDataPromise('data.json').then(function (data) {console.log(data);
});  // 后面还可以串接更多的then

Promise通过构造函数的方式创建一个Promise对象,它接受一个函数并立即执行,并向其中传入两个函数resolve, inject的引用。之后在回调还未发生的这段时间里,可以通过Promise对象的then方法继续添加回调发生以后后续要执行的逻辑(通过匿名函数),当异步回调发生时,调用resolve会依次执行then方法添加的匿名回调函数,也可以把Promise对象返回给外层调用者,串接更多的回调。这里忽略更多的细节,因为本篇不是要讲这个。

直观地看,Promise仅仅是通过对象传递上下文解决了作用域混乱的问题,但它不仅没有解决需要回调的问题,反而把问题复杂化了。它增加了更多的匿名函数,加重了垃圾回收,显然不能大量用在有性能要求的场景。但是Promise的出现是很有意义的,它的意义在于将流程控制通过Promise对象的状态来实现。说句题外话,类似的思想还有一个应用就是行为树,程序无非就是代码和数据,代码或者说流程本身既是数据,用数据去描述流程是回归本源。Promise就先说到这里。
回到同步的问题上,之所以同步的代码可以保持上下文例如局部变量,是因为上下文保存在栈上。当一个调用过程返回以后,栈的上下文就丢失了,当然在C语言中完全是这样的。而之所以匿名回调函数可以保持上下文是因为Javascript函数也是一个对象,它本身也是有上下文的,这个封闭的上下文环境就是闭包,外层函数返回以后虽然栈丢失了,但是闭包变量被保存在了匿名函数对象的上下文中。只要还持有这个匿名函数对象,这个上下文就不会丢失,我们就可以在匿名函数中访问。
举一个这样的例子:

function gen(array) {var i = 0;return function () {return array[i++];};
}
var it = gen([1, 2, 3]);
console.log(it()); // 1
console.log(it()); // 2
console.log(it()); // 3

函数gen接受一个参数,定义一个局部变量,然后返回一个匿名函数。返回的匿名函数it可以访问gen的参数和局部变量,只要我们持有这个函数对象,就可以多次调用,看起来就像重入了gen函数的栈上下文,实际上这是通过其中的匿名函数的闭包上下文实现的。
实际上这就是生成器,真正的生成器其实和这个原理一样,这里也不去讲它的语法了。它返回了一个保持上下文环境的函数对象,这样我们便可以多次调用这个函数重入生成器调用时的上下文环境。
那么这和一开始讲的异步调用有什么关系呢?前面提到过了,异步调用其实最大的问题不是代码风格,而是上下文的传递,通过全局或对象级的作用域传递上下文会干涉不相关的逻辑,而生成器既是函数,拥有函数级的词法作用域,又可以多次重入其中的上下文环境。那么我们在一个生成器中维护一个状态,控制生成的匿名函数在每次执行时执行不同的流程,在第一次调用中进行异步请求,在回调时进行第二次调用执行后续的逻辑,不就可以实现类似Promise的作用了吗?
这里就要说一下区别了,Promise是靠Promise对象去维护状态和上下文,而生成器则是靠闭包。Promise对于异步逻辑的延续是靠包装进匿名函数来实现的,越多的异步逻辑就有越多的函数对象,如前面所说存在性能问题。而生成器只在调用时创建一个匿名函数,异步逻辑都包含在这个匿名函数中,并通过一个状态(就像上面例子里的闭包变量i)来控制执行的流程。举例来说Babel和TypeScript转译出的ES5代码,可以看到它们是用switch语句来实现的,我们都知道大多数编程语言对于switch case结构都会进行一些诸如查表优化,因此生成器相比Promise效率更高一些。这里有一个测试(https://jsperf.com/v8-generators-vs-promises/11),我在chromium linux x64上测试生成器比Promise快了一倍,而这是这个例子仅使用且复用了一个匿名函数的结果(事实上v8已经可以自动优化这种情况了)。
但使用生成器其实并不比Promise容易,为了能方便地实现这一点,async方法诞生了(async方法其实在C#中早已有了,而且C#也是用了一个Task对象去维护状态,类似Promise)。再来看代码,这次我们要用到前面的getDataPromise方法:

function getDataPromise(url) {return new Promise(function(resolve, reject) {var req = new XMLHttpRequest();req.onreadystatechange = function cb () {resolve(req.responseText); // 注意这里
        };req.open('GET', url, true);req.send();});
}
async function getManyData() {var data1 = await getDataPromise('data1.json');var data2 = await getDataPromise('data2.json');return [data1, data2];
}
getManyData(); // 或await getManyData();

可以看到在getManyData这个async方法中,用await方式调用getDataPromise这个异步方法,在语法上就像是同步的,虽然实际上还是异步的。如果使用Babel或者TypeScript,编译器会把这段代码编译成生成器实现的,async方法外层包装了一层调用,如果async方法返回了一个Promise,这个包装层会在Promise对象状态改变时重入生成器,执行相应的流程。外层调用必须也是一个async方法并用await调用才能利用这种可重入上下文的特性,所以await只能在async方法中使用。从条件上来看,async也是具有传染性的,这是异步代码的根源问题,但是async方法具有函数级词法作用域,相比Promise用对象和参数保持和传递上下文更加方便灵活,这才是使用async的根本目的(还看到有人说生成的代码太丑,做前端不知道sourcemap是什么的可以去面壁了)。

虽然包装函数和生成器创建了两个函数对象,而且最终还要使用Promise包装的异步方法,但异步逻辑比较复杂时相比单纯Promise性能上有优势。前面提过v8可以优化相同函数的对象创建,所以最好仅把Promise用于包装异步API,用async方法实现业务逻辑。
其实如果用Lua的话还有一个叫做coroutine也就是协程的东西,它虽然和生成器作用类似,但是原理不同。前面可以看出来,生成器是通过闭包保持上下文的,返回时丢掉了栈上下文。而协程拥有独立的栈,就像操作系统每个线程拥有独立的栈一样,但是协程是主动返回让出执行的,线程是被动调度,协程执行时会使用自己的栈,返回时栈不会丢失,也就不会产生闭包变量。总结来说,生成器和协程前者使用闭包上下文,后者使用栈上下文,前者返回时会清栈后者只是切换调用栈,结果上来看功能是一样的。
还有一个概念叫做continuation,其实这篇就是围绕这个来讲的,本篇仅限于从JavaScript的语法角度来理解,所以不再深入下去了。讲了这么多,其实就是想说下这些东西背后的思想。

转载于:https://www.cnblogs.com/fightingCat/p/6608910.html

Javascript社区是时候接受async/await语法了相关推荐

  1. Async/Await语法糖

    Async/Await语法糖 语言层面的异步编程标准 有了 Generator 之后 js 中的异步编程基本上就已经与同步代码有类似的体验了,但是使用 Generator 这种方案我们还需要去手动编辑 ...

  2. 从不用 try-catch 实现的 async/await 语法说错误处理

    前不久看到 Dima Grossman 写的 How to write async await without try-catch blocks in Javascript.看到标题的时候,我感到非常 ...

  3. JavaScript语言核心(五)-- 异步 async/await

    迭代器 生成器 对应Python的协程 .go语言的 流程控制 转载于:https://www.cnblogs.com/rhinoxy/p/8024017.html

  4. Callbacks, Promises and Async/Await

    本文转自作者Sandeep Dinesh的文章:Callbacks, Promises and Async/Await 假设你有一个函数可以在一段随机的时间后打印一个字符串: function pri ...

  5. Python 异步 IO 、协程、asyncio、async/await、aiohttp

    From :廖雪峰 异步IO :https://www.liaoxuefeng.com/wiki/1016959663602400/1017959540289152 Python Async/Awai ...

  6. Async/Await替代Promise的6个理由

    2019独角兽企业重金招聘Python工程师标准>>> 译者按: Node.js的异步编程方式有效提高了应用性能:然而回调地狱却让人望而生畏,Promise让我们告别回调函数,写出更 ...

  7. 深入理解Async/Await

    C# 5 Async/Await 语法特性,极大地简化了异步编程,但我们知道,异步编程的基本原理并没有发生根本改变.也就是说,当一些复杂的东西看起来很简单时,它通常意味着有一些有趣的事情在背后发生.在 ...

  8. python 异步 async/await -1.一文理解什么是协程

    前言 Python 在 3.5 版本中引入了关于协程的语法糖 async 和 await, 在 python3.7 版本可以通过 asyncio.run() 运行一个协程. 所以建议大家学习协程的时候 ...

  9. 在微信小程序中使用 async/await

    微信小程序中有大量接口是异步调用,比如 wx.login() . wx.request() . wx.getUserInfo() 等,都是使用一个对象作为参数,并定义了 success() . fai ...

最新文章

  1. Science给博士新生的“欢迎”信:5点期望太扎心!
  2. org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter错误解决
  3. Boost:不受约束的bimap双图的测试程序
  4. hudson linux节点,在Linux下设置Hudson进行连续集成
  5. 此图片来自微信公众平台未经允许不可引用
  6. springboot整合swagger(高版本)异常
  7. kmap_atomic的细节以及改进
  8. Session的clear方法和flush方法
  9. 同花顺开放接口api_接口大师,即刻构建你的OpenAPI+开放平台
  10. 计量经济学计算机实验报告,计量经济学实验报告.doc
  11. windows cmd命令结束进程(window使用kill命令)
  12. 7-36 韩信点兵 (10分)
  13. 微信小程序电子优惠券领取,淘宝客,微信小程序商城
  14. linux的iptable开启命令,linux防火墙查看状态firewall、iptable
  15. 由一个误操作引起的对linux下mv命令的使用总结
  16. Android任务栈的理解
  17. DeFi之三:未来什么样——资产通证化
  18. C# 判断正负数个数
  19. ggplot2–绘制分布图
  20. 转:旅游推荐系统的演进

热门文章

  1. php中url重写,使用PHP重写URL
  2. java 打破双亲委派_JVM - 打破双亲委派机制(模拟热加载)
  3. springboot项目打包部署服务器
  4. win客户端与linux服务器C语言套接字socket
  5. 网络营销外包专员浅析尽管快照不见了网络营销外包仍在继续
  6. 网站关键词排名骤降的原因及解决办法
  7. matlab 三维饼图,重新学习MATLAB——作图技法及3D可视化
  8. (转载)Linux信息资源
  9. https ddos检测——研究现状
  10. leetcode 342. Power of Four