Spiga

写个 JavaScript 异步调用框架 (Part 1 - 问题 & 场景)

2009-05-06 00:18 by Cat Chen, 4205 visits, 收藏, 编辑

问题

在Ajax应用中,调用XMLHttpRequest是很常见的情况。特别是以客户端为中心的Ajax应用,各种需要从服务器端获取数据的操作都通过XHR异步调用完成。然而在单线程的JavaScript编程中,XHR异步调用的代码风格实在是与一般的JavaScript代码格格不入。

额外参数

考虑一个除法函数,如果它是纯客户端的同步函数,那么签名会是这样的:

function divide(operand1, operand2)

然而假设我们对客户端除法的精度不满意,于是把除法转移到服务器端来执行,那么它是个需要调用XHR的异步函数,签名也就可能会是以下几种之一:

function divide(operand1, operand2, callback)
function divide(operand1, operand2, successCallback, failureCallback)
function divide(operand1, operand2, options)

我们必须在签名中引入新的参数来传递回调函数,不能选择让函数变成阻塞式的同步调用。

可传递性

不仅仅直接操作XHR的函数需要引入新的参数,这种复杂性还会顺着调用栈向外传递。例如说,我们对加减乘除四则运算作了封装,只向外暴露一个运算接口:

function calculate(operand1, operand2, operator)

这个calculate函数根据operator参数来调用内部的plus, subtract, multiply, divide函数。然而,因为divide函数变成了异步函数,所以整个calculate函数不得不也转变为异步函数:

function calculate(operand1, operand2, operator, callback)

同时,在调用栈之上凡是需要调用到calculate的函数,都必须变成异步的,除非它并不需要向上一级调用函数返回结果。

同步并存

尽管calculate函数变成了一个异步函数,它所调用的plus, subtract, multiply函数还是同步函数,只有调用divide时是异步的,这时候calculate就是一个异步同步并存函数。

这会带来什么问题?calculate的调用者看到函数签名自然会认为calculate是个异步函数,因为它需要传递回调函数,然而calculate的执行方式却是不确定的。考虑如下调用:

calculate(operand1, operand2, operator, callback);
next();

这里涉及到callback和next两个函数,它们哪个先执行哪个后执行是不确定的,或者说是依赖于calculate具体实现的。

如果calculate的实现是,当不需要进行异步操作时,直接调用callback。那么,在执行加减乘法时callback会在next之前被调用;在执行除法时next会在callback之前调用。

如果我们不喜欢这种不确定性,我们可以改变一下calculate的实现,把同步调用也都改为setTimeout形式的,这样在任何情况下next都一定会在callback之前被调用。

然而后面一种做法依赖于成本较高的实现方式,开发者一个不小心(或者摆明偷懒)就会漏掉setTimeout,导致函数调用顺序变得不确定,所以我们会希望这是框架帮助实现的功能,在使用框架时无法把这绕过。

场景

在这里,我举一个关于上述问题的具体应用场景。(为简化问题,描述已略作修改,与实际应用并不一致。)

百度Hi网页版里面,我们会在客户端保存一个用户对象列表,在打开和这个用户的聊天窗口时,我们需要从中读取这个用户的信息。这个操作就涉及很多可能同步又可能异步的分支:

  • 用户对象未缓存
    • 异步读取用户信息
  • 用户对象已缓存
    • 用户是好友(信息更新会由服务器端推送)
      • 同步读取用户信息
    • 用户不是好友(信息更新需要由客户端拉取)
      • 可以接受缓存信息
        • 同步读取用户信息
      • 必须获取最新信息
        • 异步读取用户信息

可以看到,分支的结果最终既有同步的,也有异步的。并且这些分支还不是在一个函数里完成,而是在几个函数里实现。也就是说,按照传统的模式,这些函数一部分是同步的,一部分是异步的,由于异步的传递性,最终调用栈顶层的函数都是异步的。

为了解决这个问题,我们需要写一个异步调用框架,用一种统一的方式来进行调用,把同步和异步调用都合并为一种返回方式。

具体的解决方案会在下一篇文章中给出,如果你不希望错过的话,欢迎订阅我的博客:

Add your comment

6 条回复

  1. #1楼 一舟      2009-05-06 08:06
    自己采用OO思想封装了一个简单的Ajax调用程序。
    哈哈,说错话了。不过有俺有办法可以跑多个,曾经帮一个同事实现过。

    http://www.cnblogs.com/KiloNet/archive/2009/04/24/1443066.html
     回复 引用 查看   
  2. #2楼[楼主] Cat Chen      2009-05-06 09:24
    @一舟
    JavaScript的单线程模式是不可能被突破的,任何异步操作的回调或事件处理函数都要等到线程空闲了才可以开始执行,否则就排队。
     回复 引用 查看   
  3. #3楼 Leepy      2009-05-06 16:14
    谢谢!关注中!
     回复 引用 查看   
  4. #4楼 一舟      2009-05-06 20:25
    @Cat Chen
    呵呵。不好意思,说错话了。谢谢指正!
    刚刚做了一个测试发现,如果事件处理函数如果时间过长的话,页面也会出现假死状态!
     回复 引用 查看   
  5. #5楼[楼主] Cat Chen      2009-05-06 20:49
    @一舟
    你可以做一个简单测试,setInterval(func, 10),然后在func里面做一个10毫秒内绝对完成不了的操作,接着你会发现func的重复调用被排队了。
     回复 引用 查看   
  6. #6楼 simophin[未注册用户]2009-09-28 20:59
    嗯,凑巧了我也在写类似同异步调用的这样一个框架。不过我在同步的函数里没有使用setTimeout,而是直接呼叫传递来的callback
     回复 引用