异步javascript的原理和实现

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

我的答案当然是“能”。

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

编程平台选择没花费时间,直接选定基于Greasemonkey写自己的脚本,浏览器当然是firefox了。脚本的工作过程:

  1. 在脚本中预先存放要插入的数据
  2. 模拟鼠标点击,打开页面中的输入窗口
  3. 将数据录入到输入窗口,并模拟点击“提交”按钮,将数据提交到系统中。
  4. 依次循环,直到所有数据都处理完毕。

这里的技术难点在于:

  1. 打开输入窗口,需要等待不定期的时间,视网络情况而定。
  2. 提交数据到后台,需要等待处理完毕之后才可以循环下一个数据。

如果我是菜鸟的话,我当然直接写一个类似这样的应用逻辑:

   1:  for(var i = 0; i < dataArray.length; ++i)
   2:  {
   3:      clickButtonForInputWindow();
   4:      waitInputWindow();
   5:      enterInputData(dataArray[i]);
   6:      clickSubmitButton();
   7:      waitInputWindowClose();
   8:  }

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

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

   1:  for(var i = 0; i < dataArray.length; ++i)
   2:  {
   3:      setTimeout(clickButtonForInputWindow);
   4:  
   5:      setTimeout(waitInputWindowClose);
   6:  }

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

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

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

  1. 顺序:就是语句的先后顺序执行
  2. 判断:就是判断语句
  3. 循环:严格来说应该是跳转(goto),但大多数现代语言都取消了goto。循环其实应该是复合结构,是if和goto的组合体。

异步操作的难点在两个地方:

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

最简单的实现当然就是异步循环了,我的实现代码如下:

   1:  function asyncWhile(fn, interval) 
   2:  { 
   3:      if( fn == null || (typeof(fn) != "string" && typeof(fn) != "function") ) 
   4:          return; 
   5:      var wrapper = function() 
   6:      { 
   7:          if( (typeof(fn) == "function" ? fn() : eval(fn) ) !== false ) 
   8:              setTimeout(wrapper, interval == null? 1: interval); 
   9:      } 
  10:      wrapper(); 
  11:  }
核心内容就是:如果fn函数返回值不是false,就继续下一个setTimeout的登记调用。

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

   1:  asyncWhile(function(){
   2:      if( xxxCondition == false )
   3:          return true; // 表示继续循环
   4:      else
   5:          doSomeThing();
   6:      return false; // 表示不需要继续循环了
   7:  });

对于非等待并执行的逻辑,简单一个 setTimeout 就可以了。

异步容易,实现异步中的顺序才叫难度呢。最早的起因是我要实现3步,但是第二部是一个异步的100多次的循环。也就是说,我要实现的3步操作,其实是103次的顺序异步操作。为了一个如何在浏览器中实现可响应的等待,找破了脑袋,只找到一个firefox中的实现,还要申请特权调用。

最后想出了一个简单的方法,就是引入了“执行链(Execution Chain)”的概念,同一个执行链的所有登记函数是顺序的,不同执行链之间没有任何关系。另外,不提供互斥(mutex)等概念,如果要同步,自行在代码中检查。

在同一个执行链中,保存一个执行令牌,只有令牌和函数序号匹配,才允许执行,这样就保证了异步执行的顺序性。

   1:      function asyncSeq(funcArray, chainName, abortWhenError)
   2:      {
   3:          if( typeof(funcArray) == "function" )
   4:              return asyncSeq([funcArray], chainName, abortWhenError);
   5:              
   6:          if( funcArray == null || funcArray.length == 0 ) 
   7:              return;
   8:              
   9:          if( chainName == null ) chainName = "__default_seq_chain__";
  10:          var tInfos = asyncSeq.chainInfos = asyncSeq.chainInfos || {};
  11:          var tInfo = tInfos[chainName] = tInfos[chainName] || {count : 0, currentIndex : -1, abort : false};
  12:          
  13:          for(var i = 0; i < funcArray.length; ++i)
  14:          {
  15:              asyncWhile(function(item, tIndex){
  16:                  return function(){
  17:                      if( tInfo.abort )
  18:                          return false;
  19:                      if( tInfo.currentIndex < tIndex )
  20:                          return true;
  21:                      else if( tInfo.currentIndex == tIndex )
  22:                      {
  23:                          try{
  24:                              item();
  25:                          }
  26:                          catch(e){
  27:                              if( abortWhenError ) tInfo.abort = true;
  28:                          }
  29:                          finally{
  30:                              tInfo.currentIndex ++;
  31:                          }
  32:                      }
  33:                      else
  34:                      {
  35:                          if( abortWhenError ) tInfo.abort = true;
  36:                      }
  37:                      return false;
  38:                  };
  39:              }(funcArray[i], tInfo.count ++));
  40:          }
  41:          
  42:          setTimeout(function(){
  43:        if( tInfo.count > 0 && tInfo.currentIndex == -1 )
  44:                  tInfo.currentIndex = 0;
  45:          },20); // 为了调试的原因,加了延迟启动
  46:      }

由此,一个支持Copy&Paste的异步js函数库就完成了。具体的使用例子如下:

   1:      function testAsync()
   2:      {        
   3:          asyncSeq([function(){println("aSyncSeq -0 ");}
   4:              , function(){println("aSyncSeq -1 ");}
   5:              , function(){println("aSyncSeq -2 ");}
   6:              , function(){println("aSyncSeq -3 ");}
   7:              , function(){println("aSyncSeq -4 ");}
   8:              , function(){println("aSyncSeq -5 ");}
   9:              , function(){println("aSyncSeq -6 ");}
  10:              , function(){println("aSyncSeq -7 ");}
  11:              , function(){println("aSyncSeq -8 ");}
  12:              , function(){println("aSyncSeq -9 ");}
  13:              , function(){println("aSyncSeq -10 ");}
  14:              , function(){println("aSyncSeq -11 ");}
  15:              , function(){println("aSyncSeq -12 ");}
  16:              , function(){println("aSyncSeq -13 ");}
  17:              , function(){println("aSyncSeq -14 ");}
  18:              , function(){println("aSyncSeq -15 ");}
  19:              , function(){println("aSyncSeq -16 ");}
  20:              , function(){println("aSyncSeq -17 ");}
  21:              , function(){println("aSyncSeq -18 ");}
  22:              , function(){println("aSyncSeq -19 ");}
  23:              , function(){println("aSyncSeq -20 ");}
  24:              , function(){println("aSyncSeq -21 ");}
  25:              , function(){println("aSyncSeq -22 ");}
  26:              , function(){println("aSyncSeq -23 ");}
  27:              , function(){println("aSyncSeq -24 ");}
  28:              , function(){println("aSyncSeq -25 ");}
  29:              , function(){println("aSyncSeq -26 ");}
  30:              , function(){println("aSyncSeq -27 ");}
  31:              , function(){println("aSyncSeq -28 ");}
  32:              , function(){println("aSyncSeq -29 ");}
  33:          ]);
  34:   
  35:          asyncSeq([function(){println("aSyncSeq test-chain -a0 ");}
  36:              , function(){println("aSyncSeq test-chain -a1 ");}
  37:              , function(){println("aSyncSeq test-chain -a2 ");}
  38:              , function(){println("aSyncSeq test-chain -a3 ");}
  39:              , function(){println("aSyncSeq test-chain -a4 ");}
  40:              , function(){println("aSyncSeq test-chain -a5 ");}
  41:              , function(){println("aSyncSeq test-chain -a6 ");}
  42:              , function(){println("aSyncSeq test-chain -a7 ");}
  43:              , function(){println("aSyncSeq test-chain -a8 ");}
  44:          ], "test-chain");
  45:   
  46:          asyncSeq([function(){println("aSyncSeq -a0 ");}
  47:              , function(){println("aSyncSeq -a1 ");}
  48:              , function(){println("aSyncSeq -a2 ");}
  49:              , function(){println("aSyncSeq -a3 ");}
  50:              , function(){println("aSyncSeq -a4 ");}
  51:              , function(){println("aSyncSeq -a5 ");}
  52:              , function(){println("aSyncSeq -a6 ");}
  53:              , function(){println("aSyncSeq -a7 ");}
  54:              , function(){println("aSyncSeq -a8 ");}
  55:          ]);
  56:      }
  57:   
  58:      var textArea = null;
  59:      
  60:      function println(text)
  61:      {
  62:          if( textArea == null )
  63:          {
  64:              textArea = document.getElementById("text");
  65:              textArea.value = "";
  66:          }
  67:          
  68:          textArea.value = textArea.value + text + "\r\n";
  69:      }

最后,要向大家说一声抱歉,很多只想拿代码的朋友恐怕要失望了,如果你真的不知道怎么处理这些多余的行号,你可以学习一下正则表达式的替换,推荐用UltraEdit。

posted on 2012-11-08 02:13  老翅寒暑  阅读(8792)  评论(7编辑  收藏  举报

导航