如何实现异步代码用同步方式写(Web端)?

前端写JavaScript的时候,经常需要使用两个异步回调的方法:$.ajaxsetTimeout,这两个都是需要异步方式执行回调($.ajax可以设置为同步,但会卡住浏览器,不推荐这么使用),当有好多个$.ajax的时候,就会出现好多个回调,写起来很麻烦,甚至会出现传说中的回调金字塔,就像叠罗汉那样,Javascript有办法像Java、C#后台语言那样用同步方式写代码么?很抱歉,原生是不支持的(ES6、7会支持Promise同步调用,但需要非常高级的浏览器才支持,即使Node也不完全支持)

一般怎么写?

    var start = new Date();
   //普通jsonp调用
   $.ajax({
           type: "get",
           url: "http://tc.netease.com/zxz/datetime.php",
           dataType: "jsonp",
               success: function(__data){
                   //成功后打印时间
                   console.log(__data);
                   //相隔2秒后再次打印
                   setTimeout(function(){
                       console.log(new Date()-start);
                   },2000);
               }
           });

   setTimeout(2000);

上面是比较普遍的写法,$.ajax调用接口后,输出接口值,然后在相隔2秒后,输出时间,要是再多几个,代码就很难想象了,而且经常会有童鞋直接再调用完$.ajax就想得到接口的值,明显是不行的

可不可这样实现?

    var start = new Date();
   //调用接口,获取返回值
   var time = $.ajax("http://tc.netease.com/zxz/datetime.php");

   console.log(time);
   //停顿两秒
   setTimeout(2000);
   //再输出时间
   console.log(new Date()-start);

这里的代码,相比上面的,要清爽很多,简单、直接、明了,而且不容易犯异步请求获取数据的错。 
但显然,原生这么写,是不行的。换个思路实现呢?比如这样的:

    define(function(){

       var start = new Date();

       var time = $.ajax("http://tc.netease.com/zxz/datetime.php");

       console.log(time);

       setTimeout(2000);

       console.log(new Date()-start);
   });

用一个函数包裹着需要同步跑的代码(实际还是异步),退一步这样实现的话,貌似还能接受

实现方式

实现方式,类似于ES6、ES7的Babel转换为ES5方式的代码,这里是将函数用字符串拼接,最终使用eval来执行,而内部是用递归来实现,以下为简单实现方式,有点简单粗暴,使用上需要自行注意处理细节问题。

1、定义一个define函数,参数是函数引用,就像这样的:

    function define(func){}

2、去掉传进来函数的前缀function(){和尾部的}

    //简单粗暴的使用字符串位置来去掉
   funcStr = funcStr.substring(12,funcStr.length-1);

3、获取所有var的变量定义,以及将他们都置顶

    //置顶所有变量定义,删除所有原函数中的所有var
   var varList = funcStr.match(/var\s([0-9a-zA-Z_\$]+)[,;\s]/gi);
   evalFunc += varList.join(";") + ";";
   //把var都去掉
   funcStr = funcStr.replace(/var\s/gi,"");

4、切分代码

$.ajaxsetTimeout为分割点,将函数代码切分为对应数量*2

    //切分成多个模块
   //模块编号
   var switchIndex = 1;
   //上一个模块的字符串位置
   var lastProcessIndex = 0;
   //正则匹配,切分代码
   funcStr.replace(/(\w+\s=\s\$.ajax\([^\)]+\))|(setTimeout\(\d+\))/gi,function($a,$b){
       //拼接出代码分隔符前面的代码
       evalFunc += funcStr.substring(lastProcessIndex, funcStr.indexOf($a));

       switchIndex +=1;
       //拼接ajax或者setTimeout代码
       evalFunc += ($a.indexOf("$.ajax") > -1 ? processAjax($a,switchIndex) : processSetTimeout($a,switchIndex));

       switchIndex +=1;

       lastProcessIndex = funcStr.indexOf($a) + $a.length + 1;
   });

5、实现$.ajax和setTimeout的逻辑

function processAjax(code,index){

   //抽取请求地址
   var url = code.match(/"([^"]+)"/i)[1];
   //抽取出返回值赋值的变量名
   var param = code.match(/(\w+)\s=\s/i)[1];

   var ret = 'case '+index + ':';

   ret += '$.ajax({\
       type: "get",\
       url: "'
+url+'",\
       dataType: "jsonp",\
       success: function(__data){\
            '
+param+'=__data;\
            process('
+(index+1)+');\
       }\
   });'
;

   ret += 'break;';

   return ret;
}

function processSetTimeout(code,index){

   //抽取时间
   var time = code.match(/\d+/i)[0];

   var ret = 'case '+index + ':';

   ret += 'setTimeout(function(){\
       process('
+(index+1)+');\
   },'
+time+');';

   ret += 'break;';

   return ret;
}

6、执行拼接的字符串函数

    eval(evalFunc);

最终实现的代码

上面是实现的思路,实现细节上没有写,下面是完整的实现代码,比较简陋的方式,提供一个实现的思路

//处理ajax代码逻辑
function processAjax(code,index){

   //抽取请求地址
   var url = code.match(/"([^"]+)"/i)[1];
   //抽取出返回值赋值的变量名
   var param = code.match(/(\w+)\s=\s/i)[1];

   var ret = 'case '+index + ':';

   ret += '$.ajax({\
       type: "get",\
       url: "'
+url+'",\
       dataType: "jsonp",\
       success: function(__data){\
            '
+param+'=__data;\
            process('
+(index+1)+');\
       }\
   });'
;

   ret += 'break;';

   return ret;
}
//处理setTimeout逻辑
function processSetTimeout(code,index){

   //抽取时间
   var time = code.match(/\d+/i)[0];

   var ret = 'case '+index + ':';

   ret += 'setTimeout(function(){\
       process('
+(index+1)+');\
   },'
+time+');';

   ret += 'break;';

   return ret;
}
//具体包裹的函数实现,func为函数引用
function define(func){

   var funcStr = func.toString();

   //去掉functon和末尾的}
   funcStr = funcStr.substring(12,funcStr.length-1);
   //定义最终执行的函数字符串
   var evalFunc = "!function(){";

   //置顶所有变量定义
   var varList = funcStr.match(/var\s([0-9a-zA-Z_\$]+)[,;\s]/gi);
   evalFunc += varList.join(";") + ";";

   //把var都去掉
   funcStr = funcStr.replace(/var\s/gi,"");
   //定义内部递归调用的函数,参数为模块编号
   evalFunc += 'function process(__process_index){';
   //分割后的模块编号
   var switchIndex = 1;
   //上一个分割模块的字符串位置
   var lastProcessIndex = 0;
   //利用switch来实现针对模块编号执行
   evalFunc += 'switch(__process_index){';

   //切分成多个模块,以$.ajax和setTimeout为分隔符
   funcStr.replace(/(\w+\s=\s\$.ajax\([^\)]+\))|(setTimeout\(\d+\))/gi,function($a,$b){
       //分割符前面的代码模块
       evalFunc += 'case '+switchIndex+':';
       evalFunc += funcStr.substring(lastProcessIndex, funcStr.indexOf($a));
       //最终需要递归调用回下一个模块
       evalFunc += 'process('+(switchIndex+1)+');break;';
       //模块自增
       switchIndex +=1;
       //添加分割符的代码,$.ajax或者setTimeout
       evalFunc += ($a.indexOf("$.ajax") > -1 ? processAjax($a,switchIndex) : processSetTimeout($a,switchIndex));

       switchIndex +=1;
       //把模块字符串位置往后移动
       lastProcessIndex = funcStr.indexOf($a) + $a.length + 1;
   });

   //补齐分割完的最后的代码
   evalFunc += 'case '+switchIndex+':';
   evalFunc += funcStr.substring(lastProcessIndex);
   evalFunc += 'break;';

   evalFunc += '}';

   evalFunc += "}process(1);}();";
   //执行拼接好的函数
   eval(evalFunc);
}

//调用方式
define(function(){

   var start = new Date();

   var time = $.ajax("http://tc.netease.com/zxz/datetime.php");

   console.log(time);

   setTimeout(2000);

   console.log(new Date()-start);
});

实际上执行的代码

调用上面的define函数,实际上并非执行里面的函数引用,而是执行拼接好的函数字符串,最终上面生成的用来执行的函数会变成如下:

//自执行函数
!function(){
   //将所有变量抽取置顶后的结果
   var start ;var time ;
   //定义了递归调用的函数
   function process(__process_index){
       //用switch来执行对应的模块,下面是根据$.ajax和setTimeout切割出来的执行模块
       switch(__process_index){
           case 1:
           start = new Date();
           process(2);
           break;
       case 2:
           $.ajax({
                   type: "get",
                   url: "http://tc.netease.com/zxz/datetime.php",
                   dataType: "jsonp",
                   success: function(__data){time=__data;process(3);}
               });break;
       case 3:
           console.log(time);
           process(4);
           break;
       case 4:
           setTimeout(function(){process(5);},2000);
           break;
       case 5:
       console.log(new Date()-start);
       break;
       }
   }
   //默认执行模块1
   process(1);
}();

后记

上面的方式,用于实际项目上还需要继续调整和细节处理,提供一个可行性。 
可以依赖于构建工具或者Node等来实现将同步代码编译成异步代码执行,这样可以免去用define函数包裹

本文转载于【FEG】http://feg.netease.com/archives/357.html


本文由 w3cmark_前端笔记 版权所有,转载时请注明出处。
注明出处格式:w3cmark (http://www.w3cmark.com/2016/501.html)

分享到:

评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)
关注w3cmark
微信公众号 w3cmark_com