异步javascript的原理和实现技巧介绍


Posted in Javascript onNovember 08, 2012

因为工作的需要,我要在网页端编写一段脚本,把数据通过网页批量提交到系统中去。所以我就想到了Greasemonkey插件,于是就开始动手写,发现问题解决得很顺利。但是在对脚本进行总结和整理的时候,我习惯性地问了自己一个问题:能不能再简单点?
我的答案当然是“能”。

首先回顾我的数据批量提交的需求:我有一批用户数据要插入到系统中,但是因为系统库表结构不是行列式的,所以无法转化为sql语句插入。要插入的数据有接近200条,就是傻呵呵地手工录入到系统,估计也要1天的时间。作为程序员,当然不会干这么傻的事情,我一定要用程序来解决。这个编程的过程耗费了我1天的时间。相比手工录入,我额外收入是这篇博文,绝对的合算!

编程平台选择没花费时间,直接选定基于Greasemonkey写自己的脚本,浏览器当然是firefox了。脚本的工作过程:
在脚本中预先存放要插入的数据
模拟鼠标点击,打开页面中的输入窗口
将数据录入到输入窗口,并模拟点击“提交”按钮,将数据提交到系统中。
依次循环,直到所有数据都处理完毕。

这里的技术难点在于:
打开输入窗口,需要等待不定期的时间,视网络情况而定。
提交数据到后台,需要等待处理完毕之后才可以循环下一个数据。
如果我是菜鸟的话,我当然直接写一个类似这样的应用逻辑:

for(var i = 0; i < dataArray.length; ++i) 
{ 3: clickButtonForInputWindow(); 
waitInputWindow(); 
enterInputData(dataArray[i]); 
clickSubmitButton(); 
waitInputWindowClose(); 
}

实际上这样写所有浏览器都会陷入一片白屏,并在若干分钟之后提示“没有响应”而被强行终止掉。原因就是浏览器在调用javascript的时候,主界面是停止响应的,因为cpu交给js执行了,没有时间去处理界面消息。

为了满足“不锁死”的要求,我们可以把脚本修改成这样:

for(var i = 0; i < dataArray.length; ++i) 
{ 
setTimeout(clickButtonForInputWindow); 
… 
setTimeout(waitInputWindowClose); 
}

实际上setTimeout和setInterval是浏览器唯一可以支持异步的操作。如何更优雅地使用这两个函数来实现异步操作呢?目前简单的答案是老赵的Wind.js。虽然我没有用过这个函数库,但是光是$await调用,就是符合我一贯对简洁的要求的。但是对于我这样的单个文件的脚本来说,去网上下载一个外部js库,明显不如有一段支持异步操作的代码拷贝过来的快和爽。

所以我决定另辟蹊径,做一个不要编译而且易用性还可以更能够Copy&Paste的异步函数库。

说异步之前,我们一起回忆一下同步操作的几种结构类型:

顺序:就是语句的先后顺序执行
判断:就是判断语句
循环:严格来说应该是跳转(goto),但大多数现代语言都取消了goto。循环其实应该是复合结构,是if和goto的组合体。
异步操作的难点在两个地方:

异步的判断:异步情况下的判断基本都是检测条件十分满足,然后执行某些动作。
异步的顺序:顺序中的每一步操作之后都要交回控制权,等待在下一个时间片中继续执行下一步。难点是如何保持顺序性。尤其在两个顺序动作中间夹杂一个异步的循环的时候。
异步的循环:每次循环之后都交回控制权到浏览器,如此循环,直到运行结束。
最简单的实现当然就是异步循环了,我的实现代码如下:

function asyncWhile(fn, interval) 
{ 
if( fn == null || (typeof(fn) != "string" && typeof(fn) != "function") ) 
return; 
var wrapper = function() 
{ 
if( (typeof(fn) == "function" ? fn() : eval(fn) ) !== false ) 
setTimeout(wrapper, interval == null? 1: interval); 
} 
wrapper(); 
}

核心内容就是:如果fn函数返回值不是false,就继续下一个setTimeout的登记调用。

实际上,“等待并执行”逻辑,根本上就是一个异步循环问题。这种情况的实现方法示例如下:

asyncWhile(function(){ 
if( xxxCondition == false ) 
return true; // 表示继续循环 
else 
doSomeThing(); 
return false; // 表示不需要继续循环了 
});

对于非等待并执行的逻辑,简单一个 setTimeout 就可以了。
异步容易,实现异步中的顺序才叫难度呢。最早的起因是我要实现3步,但是第二部是一个异步的100多次的循环。也就是说,我要实现的3步操作,其实是103次的顺序异步操作。为了一个如何在浏览器中实现可响应的等待,找破了脑袋,只找到一个firefox中的实现,还要申请特权调用。
最后想出了一个简单的方法,就是引入了“执行链(Execution Chain)”的概念,同一个执行链的所有登记函数是顺序的,不同执行链之间没有任何关系。另外,不提供互斥(mutex)等概念,如果要同步,自行在代码中检查。
在同一个执行链中,保存一个执行令牌,只有令牌和函数序号匹配,才允许执行,这样就保证了异步执行的顺序性。
function asyncSeq(funcArray, chainName, abortWhenError) 
{ 
if( typeof(funcArray) == "function" ) 
return asyncSeq([funcArray], chainName, abortWhenError); if( funcArray == null || funcArray.length == 0 ) 
return; 
if( chainName == null ) chainName = "__default_seq_chain__"; 
var tInfos = asyncSeq.chainInfos = asyncSeq.chainInfos || {}; 
var tInfo = tInfos[chainName] = tInfos[chainName] || {count : 0, currentIndex : -1, abort : false}; 
for(var i = 0; i < funcArray.length; ++i) 
{ 
asyncWhile(function(item, tIndex){ 
return function(){ 
if( tInfo.abort ) 
return false; 
if( tInfo.currentIndex < tIndex ) 
return true; 
else if( tInfo.currentIndex == tIndex ) 
{ 
try{ 
item(); 
} 
catch(e){ 
if( abortWhenError ) tInfo.abort = true; 
} 
finally{ 
tInfo.currentIndex ++; 
} 
} 
else 
{ 
if( abortWhenError ) tInfo.abort = true; 
} 
return false; 
}; 
}(funcArray[i], tInfo.count ++)); 
} 
setTimeout(function(){ 
if( tInfo.count > 0 && tInfo.currentIndex == -1 ) 
tInfo.currentIndex = 0; 
},20); // 为了调试的原因,加了延迟启动 
}

由此,一个支持Copy&Paste的异步js函数库就完成了。具体的使用例子如下:
function testAsync() 
{ 
asyncSeq([function(){println("aSyncSeq -0 ");} 
, function(){println("aSyncSeq -1 ");} 
, function(){println("aSyncSeq -2 ");} 
, function(){println("aSyncSeq -3 ");} 
, function(){println("aSyncSeq -4 ");} 
, function(){println("aSyncSeq -5 ");} 
, function(){println("aSyncSeq -6 ");} 
, function(){println("aSyncSeq -7 ");} 
, function(){println("aSyncSeq -8 ");} 
, function(){println("aSyncSeq -9 ");} 
, function(){println("aSyncSeq -10 ");} 
, function(){println("aSyncSeq -11 ");} 
, function(){println("aSyncSeq -12 ");} 
, function(){println("aSyncSeq -13 ");} 
, function(){println("aSyncSeq -14 ");} 
, function(){println("aSyncSeq -15 ");} 
, function(){println("aSyncSeq -16 ");} 
, function(){println("aSyncSeq -17 ");} 
, function(){println("aSyncSeq -18 ");} 
, function(){println("aSyncSeq -19 ");} 
, function(){println("aSyncSeq -20 ");} 
, function(){println("aSyncSeq -21 ");} 
, function(){println("aSyncSeq -22 ");} 
, function(){println("aSyncSeq -23 ");} 
, function(){println("aSyncSeq -24 ");} 
, function(){println("aSyncSeq -25 ");} 
, function(){println("aSyncSeq -26 ");} 
, function(){println("aSyncSeq -27 ");} 
, function(){println("aSyncSeq -28 ");} 
, function(){println("aSyncSeq -29 ");} 
]); asyncSeq([function(){println("aSyncSeq test-chain -a0 ");} 
, function(){println("aSyncSeq test-chain -a1 ");} 
, function(){println("aSyncSeq test-chain -a2 ");} 
, function(){println("aSyncSeq test-chain -a3 ");} 
, function(){println("aSyncSeq test-chain -a4 ");} 
, function(){println("aSyncSeq test-chain -a5 ");} 
, function(){println("aSyncSeq test-chain -a6 ");} 
, function(){println("aSyncSeq test-chain -a7 ");} 
, function(){println("aSyncSeq test-chain -a8 ");} 
], "test-chain"); 
asyncSeq([function(){println("aSyncSeq -a0 ");} 
, function(){println("aSyncSeq -a1 ");} 
, function(){println("aSyncSeq -a2 ");} 
, function(){println("aSyncSeq -a3 ");} 
, function(){println("aSyncSeq -a4 ");} 
, function(){println("aSyncSeq -a5 ");} 
, function(){println("aSyncSeq -a6 ");} 
, function(){println("aSyncSeq -a7 ");} 
, function(){println("aSyncSeq -a8 ");} 
]); 
} 
var textArea = null; 
function println(text) 
{ 
if( textArea == null ) 
{ 
textArea = document.getElementById("text"); 
textArea.value = ""; 
} 
textArea.value = textArea.value + text + "\r\n"; 
}

最后,要向大家说一声抱歉,很多只想拿代码的朋友恐怕要失望了,如果你真的不知道怎么处理这些多余的行号,你可以学习一下正则表达式的替换,推荐用UltraEdit。
Javascript 相关文章推荐
一段批量给页面上的控件赋值js
Jun 19 Javascript
jsp js鼠标移动到指定区域显示选项卡离开时隐藏示例
Jun 14 Javascript
jQuery动态设置form表单的enctype值(实现代码)
Jul 04 Javascript
js实现收缩菜单效果实例代码
Oct 30 Javascript
Javascript常用字符串判断函数代码分享
Dec 08 Javascript
jQuery常用选择器详解
Jul 17 jQuery
Vue脚手架的简单使用实例
Jul 10 Javascript
微信小程序实现两个页面传值的方法分析
Dec 11 Javascript
vue axios封装及API统一管理的方法
Apr 18 Javascript
JS字符串补全方法padStart()和padEnd()
May 27 Javascript
Vue如何基于vue-i18n实现多国语言兼容
Jul 17 Javascript
JS闭包原理及其使用场景解析
Dec 03 Javascript
找出字符串中出现次数最多的字母和出现次数精简版
Nov 07 #Javascript
jquery 如何动态添加、删除class样式方法介绍
Nov 07 #Javascript
探索Emberjs制作一个简单的Todo应用
Nov 07 #Javascript
关于使用 jBox 对话框的提交不能弹出问题解决方法
Nov 07 #Javascript
seajs1.3.0源码解析之module依赖有序加载
Nov 07 #Javascript
Javascript引用指针使用介绍
Nov 07 #Javascript
JavaScript在多浏览器下for循环的使用方法
Nov 07 #Javascript
You might like
ThinkPHP行为扩展Behavior应用实例详解
2014/07/22 PHP
php+mysql实现数据库随机重排实例
2014/10/17 PHP
php创建和删除目录函数介绍和递归删除目录函数分享
2014/11/18 PHP
php获取百度收录、百度热词及百度快照的方法
2015/04/02 PHP
PHP常用处理静态操作类
2015/04/03 PHP
php+ajax实现无刷新分页
2015/11/18 PHP
WordPress导航菜单的滚动和淡入淡出效果的实现要点
2015/12/14 PHP
PHP错误处理函数
2016/04/03 PHP
PHP之图片上传类实例代码(加了缩略图)
2016/06/30 PHP
Laravel中批量赋值Mass-Assignment的真正含义详解
2017/09/29 PHP
thinkphp框架类库扩展操作示例
2019/11/26 PHP
JS无限极树形菜单,json格式、数组格式通用示例
2013/07/30 Javascript
jquery如何通过name名称获取当前name的value值
2013/12/20 Javascript
js操纵dom生成下拉列表框的方法
2014/02/24 Javascript
JS对img标签进行优化使用onerror显示默认图像
2014/04/24 Javascript
Bootstrap每天必学之导航条(二)
2016/03/01 Javascript
vue实现todolist单页面应用
2017/04/11 Javascript
详解angular分页插件tm.pagination二次触发问题解决方案
2018/07/20 Javascript
bootstrap table实现横向合并与纵向合并
2019/07/18 Javascript
Vue学习之常用指令实例详解
2020/01/06 Javascript
解决ant Design中Select设置initialValue时的大坑
2020/10/29 Javascript
vue+iview实现分页及查询功能
2020/11/17 Vue.js
python的几种开发工具介绍
2007/03/07 Python
Python去除、替换字符串空格的处理方法
2018/04/01 Python
pandas apply 函数 实现多进程的示例讲解
2018/04/20 Python
cmd运行python文件时对结果进行保存的方法
2018/05/16 Python
python3+selenium自动化测试框架详解
2019/03/17 Python
python实现统计文本中单词出现的频率详解
2019/05/20 Python
PyQt5固定窗口大小的方法
2019/06/18 Python
python与pycharm有何区别
2020/07/01 Python
“型”走纽约上东区:Sam Edelman
2017/04/02 全球购物
医院学雷锋活动策划方案
2014/02/15 职场文书
化学专业自荐信
2014/05/28 职场文书
建筑管理专业求职信
2014/07/28 职场文书
2014小学语文教师个人工作总结
2014/12/03 职场文书
2015年工程师工作总结
2015/04/30 职场文书