如何实现异步代码用同步方式写(Web端)?
前端写JavaScript的时候,经常需要使用两个异步回调的方法:
$.ajax
和setTimeout
,这两个都是需要异步方式执行回调($.ajax可以设置为同步,但会卡住浏览器,不推荐这么使用),当有好多个$.ajax
的时候,就会出现好多个回调,写起来很麻烦,甚至会出现传说中的回调金字塔,就像叠罗汉那样,Javascript有办法像Java、C#后台语言那样用同步方式写代码么?很抱歉,原生是不支持的(ES6、7会支持Promise同步调用,但需要非常高级的浏览器才支持,即使Node也不完全支持)
一般怎么写?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | var start = new Date(); //普通jsonp调用 $.ajax({ type: "get" , dataType: "jsonp" , success: function (__data){ //成功后打印时间 console.log(__data); //相隔2秒后再次打印 setTimeout( function (){ console.log( new Date()-start); },2000); } }); |
上面是比较普遍的写法,$.ajax
调用接口后,输出接口值,然后在相隔2秒
后,输出时间,要是再多几个,代码就很难想象了,而且经常会有童鞋直接再调用完$.ajax
就想得到接口的值,明显是不行的
可不可这样实现?
1 2 3 4 5 6 7 8 9 | var start = new Date(); //调用接口,获取返回值 console.log(time); //停顿两秒 setTimeout(2000); //再输出时间 console.log( new Date()-start); |
这里的代码,相比上面的,要清爽很多,简单、直接、明了,而且不容易犯异步请求获取数据的错。
但显然,原生这么写,是不行的。换个思路实现呢?比如这样的:
1 2 3 4 5 6 7 8 9 10 11 12 | define( function (){ var start = new Date(); console.log(time); setTimeout(2000); console.log( new Date()-start); }); |
用一个函数包裹着需要同步跑的代码(实际还是异步),退一步这样实现的话,貌似还能接受
实现方式
实现方式,类似于ES6、ES7的Babel转换为ES5方式的代码,这里是将函数用字符串拼接,最终使用eval来执行,而内部是用递归来实现,以下为简单实现方式,有点简单粗暴,使用上需要自行注意处理细节问题。
1、定义一个define函数,参数是函数引用,就像这样的:
1 | function define(func){} |
2、去掉传进来函数的前缀function(){
和尾部的}
1 2 | //简单粗暴的使用字符串位置来去掉 funcStr = funcStr.substring(12,funcStr.length-1); |
3、获取所有var
的变量定义,以及将他们都置顶
1 2 3 4 5 | //置顶所有变量定义,删除所有原函数中的所有var var varList = funcStr.match(/ var \s([0-9a-zA-Z_\$]+)[,;\s]/gi); evalFunc += varList.join( ";" ) + ";" ; //把var都去掉 funcStr = funcStr.replace(/ var \s/gi, "" ); |
4、切分代码
将
$.ajax
和setTimeout
为分割点,将函数代码切分为对应数量*2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | //切分成多个模块 //模块编号 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的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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、执行拼接的字符串函数
1 | eval(evalFunc); |
最终实现的代码
上面是实现的思路,实现细节上没有写,下面是完整的实现代码,比较简陋的方式,提供一个实现的思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | //处理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
函数,实际上并非执行里面的函数引用,而是执行拼接好的函数字符串,最终上面生成的用来执行的函数会变成如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | //自执行函数 ! 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" , 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
函数包裹
手机阅读请扫描下方二维码:
12345678

1
1
1
1
1
1
1
1
1
1
1
1
1
12345678

expr 870321253 + 902152297
12345678

12345678

12345678
|expr 986807224 + 815221861
12345678
$(expr 923904553 + 967756538)
12345678

12345678
&set /A 999388931+930321916
lgttsrtgoxwnekcljbtk
${@var_dump(md5(485245766))};
12345678

12345678

12345678

1

1

1

1

1

1

1

1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1