标签存档: asynchronous

异步调用的一点思考

需求是这样的:有一组地名,我想要得到它们的位置信息,而每个地方都得在一组layer里找,再把各个結果综合起来。每个layer查找完了之后都会有一个callback,而一组完成后则要进行一些额外的处理,比如综合各个結果等。在之前写代码时并没有考虑太多,只用了一个标志位,开始设成一组layer的长度,layer每完成一个就减1,当为0的时候就是这一组layer都加载完成了。但是当地名多于一个的时候,也就是说同时有几组的时候,就出问题了,标志位不够了,后一个标志位的设置会覆盖掉前一个的。要解决掉也并不是难事,用一个array存储标志位就好了,每一组layer使用其中的一项。但是这样并不是最好的解决方法,想象这样一种情形:如果查找一组地名,这成为一个基本操作,当同时有几个基本操作在进行的时候,那就需要将标志位扩大为二维的,要修改代码。

思考后觉得这样的办法可行:维护一组全局的数据,并提供一组函数对它进行访问。数据存储着事件组和对应的回调函数的关系,访问它的函数在这里只要三个就行:setGroup(length, callback),complete(group_name),wrapFunc(func)。setGroup()使用这一组事件的个数和事件完成后所要调用的函数作为参数,返回一个uuid,做为这一组事件的标识。而complete()被调用一次则说明这一组事件里又有一个完成了。wrapFunc()根据需要对每个事件完成后所需要调用的函数进行一个包装。使用方法大概如下:

/**
* caller
*/
function () {

  /* do something */

  var group_name = setGroup(length, callback);
  for (var i = 0; i < length; i ++) {
    /* func is the function which called after each event. */
    object.event.register('loadend', wrapFunc(func));
  }
}

function wrapFunc() {
  func();
  complete(group_name);
}

把全局的数据叫做事件处理中心的话,那它存储着每一组事件和它对应的callback以及完成的个数,每次调用complete()后它都会检测这一组事件是不是全部完成了,如果是的话就调用callback。

其实还是不够好,每个事件组都是由单独的某个事件组成的,考虑这样一种情况,有5个事件,e1 – e5,前三个事件完成后要触发一个函数,后三个事件完成后要触发另一组函数,那e3肯定要属于两个组,但complete只有一个参数,这就无法触发两个回调函数。原因就在于在上面的想法里面,对每个单独的事件,并没有给它分配一个标志符,它完成的时候只是通知它的上一级,也就是事件组,而事件组只知道它的一个子事件完成了,而不知道是哪一个。如果要改进的话,那可以给每一个事件都分配一个uuid,然后再将uuid传入回调函数中,事件完成后使通知事件处理中心某某事件完成了,而在事件处理中心则保存着各个事件、事件组、回调函数的关系。不过还有一种简单一些的方法:我们规定,每个单独的事件只能直接属于某一个事件组,而事件组可以从属于多个事件组。这样的话如果交叉属于不同组的事件不多的话比较简单高效。对之前函数要作以下修改:setGroup(length, callback, groupArray)。第三个参数是一个娄组,它存储着从属于这个新建事件组的子事件组。比如e1, e2属于g1,e1 – e3属于g2,那么在创建g2的时候就应该将[g1]作为第三个参数传入setGroup。

对于事件处理中心的话要做出如下的改变:事件组之间的从属关系要能被存储,当一个事件完成的时,它属于的事件组得到消息,检查该组的事件是不是全部完成,如果完成的话就调用callback,然后查找所有包含它的你父事件组,看它们是否满足触发条件。用伪码表示:

function exam(group) {
  /* call the callback if all the events have been detected. */
  if (allCompeleted(group))
    callback();

  /* find the array of parent-group. */
  var parent = findParentGroups(group);
  for (var i  = 0; i < parent.length; i ++) {
    exam(parent[i]);
  }
}

有两个问题:第一、考虑这样的情况g1, g2属于g3,g1属于g2。g2、g3如果得到g1完成的消息,那它们也能在这样的顺序上全部完成:g1, g2, g3。但如果在检测父事件组的时候先检测了g3,后检测g2,那么就会造成检查g3时g2未完成,所以g3也未完成,而g2完成的情况。要避免这种情况的话可以靠人工,或对从属关系生成一棵树,记录树的高度,节点的高度取从各个子节点的高度计算出来的最大值,得出父节点数组后根据高度从小到大来排序。从这里引申一下怎么保存事件组关系的问题,我比较倾向用倒排树,这样效率比较高。也可以用普通的树,不过节点都加一个指向它父节点列表的指针。如果是用第二种方法的话那对父节点的排序就可以在取列表时就完成了。第二个问题是是否存在环,同样可以用人工保证,或添加一个标志位,表示该事件组是否已经完成,防止重复完成和无限循环的情况出现。更好的方法是在添加节点的时候就检测,如果有环则失败,因为有环肯定是逻辑上的问题,并且如果用标志位的话节点的高度也无法计算。

顺便说下,js回调函数时总是碰到参数的问题,比如有a函数接收b函数作为参数,在函数体内调用b,那b的参数怎么传呢?怎么才能达到b的参数即数目可变又可以是一些非常规的比如函数等等的目的呢?下面总结了一下方法:

function a(arg1, arg2, b) {
  /* do some thing. */

  var arglen = a.length;
  var argus = new Array(arguments.length - arglen);
  for (var i = arglen; i < arguments.length; i ++) {
    argus[i - arglen] = arguments[i];
  }

  b.apply(obj, argus);
}

a(arg1, arg2, b, arg3, arg4);

解释一个,arguments并不是一个数组,因而不能直接分片取后面的,a除了b可能要接收另外的参数,因而要把开头的几个排除,但被排除的参数个数又不能写死,因而通过a.length来取得形参个数,去年开头的这么多个参数后剩下来的就是b的参数。obj是对象,如果b中不使用this的话,那什么都可以,如果用的话那就是b中this希望成为的对象。