基于浏览器的事件轮询机制(以及Node.js中的事件轮询机制),JavaScript经常会运行在异步环境中。因为JavaScript自己语言的特性(不须要程序员操控线程/进程),在js中解决异步化编程的方法就显得至关重要。能够说一个完整的项目中,js开发人员是不可能不面对异步操做的。本文将详细介绍几种经典JavaScript异步编程串行化方法,同时也将简单介绍一下ES6提供的Promise顺序执行方法。前端
假设咱们有一个ajax()
方法,他接收一个url参数,向该地址发起一个异步请求,在请求结束时执行第二个参数—一个回调函数:git
ajax(url,function(result){ console.log(result); });
能够说这种方式几乎是每一个前端开发人员都用过的回调函数方式,有了这样的回调机制,开发人员就不用编写相似下面这样的代码来推测服务器请求何时返回:程序员
var result=ajax(url); setTimeout(function(result){ console.log(result); },400);
你们应该能明白我此处想表达的意思。咱们设置了一个延迟400毫秒的定时器,来假设咱们发出的ajax请求会在400毫秒以内完成。不然,咱们将会操做一个undefined
的result
。
可是有一个问题随着项目的扩大渐渐浮现出来:若是场景须要咱们多层嵌套回调函数,代码将变得难以阅读和维护:github
ajax(url0,function(result0){ ajax(result0.url1,function(result1){ ajax(result1.url2,function(result2){ console.log(result2); }); }); });
为了解决内联回调函数暴露出来的代码混乱问题,咱们引入外部函数调用来解决相似问题:web
function handle2(result){ console.log(result); } function handle1(result){ ajax(result.url,function(result){ handle2(result); }); } ajax(url,function(result){ handle1(result); });
经过这种拆份内联函数,来调用外部函数的优化方法,能极大的保持代码的简洁性。ajax
观察流行的JavaScript流程控制工具,例如Nimble、Step、Seq,咱们会学习到一种简洁的设计模式:经过回调管理器来控制异步JavaScript执行流程,下面是一个典型的回调管理器的关键代码示例:编程
var Flow={}; //设置next方法,在上一个方法完成时调用下一个方法 Flow.next=function(){ if(this.stack[0]){ //弹出方法栈中的第一个方法,并执行他 this.stack.shift()(); } }; //设置series方法,接收一个函数数组,并按序执行 Flow.series=function(arr){ this.stack=arr; this.next(); }; //经过Flow.series咱们可以控制传入的函数的执行顺序 Flow.series([ function(){ $.ajax({ url:"xxx.xxx", params:"xxx", success:function(response){ //do something Flow.next(); } }); }, function(){ setTimeout(function(){ //do something Flow.next(); },1000); } ]);
咱们初始化了一个Flow
控制器,为他设计了series
和next
两个函数属性。在咱们编写的业务方法内部,在方法结尾处经过调用Flow.next()
的方式来顺序触发下一个方法;经过执行series
方法来顺序执行异步函数。这种经过核心控制器来管理异步函数调用的方式简化了咱们的编程过程,让开发人员可以投入更多精力在业务逻辑上。后端
也许上面介绍的异步方法仍然不能知足实际开发中的业务场景:假设咱们有a()
,b()
,c()
三个方法,a和b没有依赖关系,能够异步进行。可是c必须在a和b都完成以后才能触发。为知足这样的逻辑实现,咱们加入一个全局计数器来控制代码的执行流程:设计模式
var flag=2; var aValue,bValue; function a(){ aValue=1; flag--; c(); } function b(){ setTimeout(function(){ bValue=2; flag--; c(); },200); } function c(){ if(flag==0){ console.log("after a and b:"+(aValue+bValue)); } } a(); b();
咱们设置了一个全局变量flag来监控方法a和方法b的完成状况。方法b经过设置一个200毫秒的定时器来模拟网络环境,最终会在b方法执行完成以后成功调用c方法。这样咱们就实现了对方法a()
,b()
,c()
的依赖调用。数组
当上述方案在复杂场景下应用时,会出现以下问题:产品通过多个版本迭代,c方法依赖更多的方法,所以计数器flag须要不断的变化;产品迭代过程当中更换了开发人员。当出现上述两种状况时,代码的逻辑将会变得混乱不堪,flag标记符是否能保持简明正确很大程度上受到了产品迭代的影响。所以咱们提出面向数据的优化改进。
在真实的开发场景中,存在方法依赖的缘由基本都是由于存在数据依赖,对于上面那个简单的示例:c方法依赖于a方法和b方法操做的结果,而不是依赖于flag是否为0。所以咱们能够经过检查依赖方法是否已经完成了数据处理来代替检查标记符是否已经被置为0,在这个例子中也就是在c方法中检查aValue和bValue是否已经完成了赋值:
function c(){ if(aValue!==undefined && bValue!==undefined){ console.log("after a and b:"+(aValue+bValue)); } }
针对更加通用的场景,咱们将上述代码修改成下:
var checkDependency={}; var aValue,bValue; function a(){ aValue=1; checkDependency.a=true; c(); } function b(){ setTimeout(function(){ bValue=2; checkDependency.b=true; c(); },200); } function c(){ if(checkDependency.a && checkDependency.b){ console.log("after a and b:"+(aValue+bValue)); } } a(); b();
经过面向数据的检查方式,将来扩展时,咱们仅须要在新增的方法中增长对checkDependency
对象的修改,而且在c方法中检查相应属性的存在就能实现异步依赖方法的顺序执行。
为了解决JavaScript中异步方法的复杂性,官方引入了一种统一的控制方式:
var bool=false; /* * 新建一个Promise实例,向构造函数传入一个异步执行函数 * 异步函数会接受两个参数,由Promise传入,对应then方法中传入的方法 */ var promise=new Promise(function(resolve,reject){ setTimeout(function(){ if(bool){ //根据执行状况相应调用resolve和reject resolve(bool); }else{ reject(bool); } },200); }); //经过then向Promise实例传入解决方法 promise.then(function resolve(result){ console.log("success"); },function reject(result){ console.log("failure"); });
上例代码展现了一个基础的Promise应用,也许实际场景中更加多见的是下面这种链式调用:
new Promise(function(res,rej){ if(/*异步调用成功*/){ res(data); }else{ rej(error); } }).then(function resolve(result){ console.log("success"); },function reject(result){ console.log("failure"); });
若是对Promise感兴趣的话,能够在网上寻找资料继续深刻学习!
关于Promise的兼容性,一般web前端JavaScript代码中不会直接使用Promise(经过caniuse.com网站查询发现Android4.4不支持Promise)。若是特别想使用的,每每会在项目中附带一些补足兼容性的promise类库;然后端Node.js能够放心使用Promise类来管理异步逻辑。