异步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 相关文章推荐
来自qq的javascript面试题
Jul 24 Javascript
JS实现标签页效果(配合css)
Apr 03 Javascript
jQuery下实现等待指定元素加载完毕(可改成纯js版)
Jul 11 Javascript
jQuery中animate动画第二次点击事件没反应
May 07 Javascript
JavaScript的面向对象编程基础
Aug 13 Javascript
详解js中构造流程图的核心技术JsPlumb(2)
Dec 08 Javascript
详解jQuery停止动画——stop()方法的使用
Dec 14 Javascript
AngularJS双向绑定和依赖反转实例详解
Apr 15 Javascript
jQuery实现动态加载select下拉列表项功能示例
May 31 jQuery
JavaScript设计模式之享元模式实例详解
Jan 17 Javascript
微信小程序可滑动月日历组件使用详解
Oct 21 Javascript
js布局实现单选按钮控件
Jan 17 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
在PHP中使用redis
2013/11/04 PHP
PHP获取浏览器信息类和客户端地理位置的2个方法
2014/04/24 PHP
php关键字仅替换一次的实现函数
2015/10/29 PHP
对比分析php中Cookie与Session的异同
2016/02/19 PHP
php微信公众号开发(3)php实现简单微信文本通讯
2016/12/15 PHP
Laravel框架实现的批量删除功能示例
2019/01/16 PHP
Swoole4.4协程抢占式调度器详解
2019/05/23 PHP
jquery写个checkbox——类似邮箱全选功能
2013/03/19 Javascript
javascript中的document.open()方法使用介绍
2013/10/09 Javascript
jQuery Ajax异步处理Json数据详解
2013/11/05 Javascript
Js冒泡事件详解及阻止示例
2014/03/21 Javascript
百度地图自定义控件分享
2015/03/04 Javascript
jQuery插件uploadify实现ajax效果的图片上传
2016/06/18 Javascript
JS定时器用法分析【时钟与菜单中的应用】
2016/12/21 Javascript
Vue.js实现简单动态数据处理
2017/02/13 Javascript
微信小程序中实现一对多发消息详解及实例代码
2017/02/14 Javascript
详解Vue 开发模式下跨域问题
2017/06/06 Javascript
详解使用Node.js 将txt文件转为Excel文件
2017/07/05 Javascript
浅谈mvvm-simple双向绑定简单实现
2018/04/18 Javascript
浅谈Vue 自动化部署打包上线
2020/06/14 Javascript
JavaScript实现原型封装轮播图
2020/12/27 Javascript
Python标准库之循环器(itertools)介绍
2014/11/25 Python
Python在Console下显示文本进度条的方法
2016/02/14 Python
详解Python读取yaml文件多层菜单
2019/03/23 Python
Python进阶:生成器 懒人版本的迭代器详解
2019/06/29 Python
python从list列表中选出一个数和其对应的坐标方法
2019/07/20 Python
python飞机大战pygame游戏背景设计详解
2019/12/17 Python
Python使用tkinter制作在线翻译软件
2021/02/22 Python
使用html5制作loading图的示例
2014/04/14 HTML / CSS
中国电子产品外贸网站:MiniIntheBox
2017/02/06 全球购物
英国在线珠宝店:The Jewel Hut
2017/03/20 全球购物
英国在线汽车和面包车零件商店:Car Parts 4 Less
2018/08/15 全球购物
房地产活动策划方案
2014/05/14 职场文书
地道战观后感500字
2015/06/04 职场文书
CSS基础详解
2021/10/16 HTML / CSS
SQL基础的查询语句
2021/11/11 MySQL