联系电话:020-28187900

AJAX 开发中的难题

发表时间:2012-09-25 阅读: 2921

假设你要建立一个树形结构的公告栏系统(BBS),它可以根据用户请求与服务器进行交互,动态加载每篇文 章的信息,而不是一次性从服务器载入所有文章信息。每篇文章有四个相关属性:系统中可以作为唯一标识的ID、发贴人姓名、文章内容以及包含其所有子文章 ID的数组信息。首先假定有一个名为getArticle()的函数可以加载一篇文章信息。该函数接收的参数是要加载文章的ID,通过它可从服务器获取文 章信息。它返回的对象包含与文章相关的四条属性:id,name,content和children。例程如下:

function ( id ) {      var a = getArticle(id);      document.writeln(a.name + " " + a.content);  }
然而你也许会注意到,重复用同一个文章ID调用此函数,需要与服务器之间进行反复且无益的通信。想要解决这个问题,可以考虑使用函数 getArticleWithCache(),它相当于一个带有缓存能力的getArticle()。在这个例子中,getArticle()返回的数据 只是作为一个全局变量被保存下来:

var cache = {};  function getArticleWithCache ( id ) {      if ( !cache[id] ) {          cache[id] = getArticle(id);      }      return cache[id];  }
现在已将读入的文章缓存起来,让我们再来考虑一下函数backgroundLoad(),它应用我们上面提到的缓存机制加载所有文章信息。其用途 是,当读者在阅读某篇文章时,从后台预加载它所有子文章。因为文章数据是树状结构的,所以很容易写一个递归的算法来遍历树并且加载所有的文章:

function backgroundLoad ( ids ) {      for ( var i=0; i < ids.length; i++ ) {          var a = getArticleWithCache(ids[i]);          backgroundLoad(a.children);      }  }
backgroundLoad ()函数接收一个ID数组作为参数,然后通过每个ID调用前面定义过的getArticldWithCache()方法,这样就把每个ID对应的文章缓存 起来。之后再通过已加载文章的子文章ID数组递归调用backgroundLoad()方法,如此整个文章树就被缓存起来。

到目前为止,一切似乎看起来都很完美。然而,只要你有过开发AJAX应用的经验,你就应该知晓这种幼稚的实现方法根本不会成功,这个例子成立的基础 是默认 getArticle()用的是同步通信。可是,作为一条基本原则,JavaScript要求在与服务器进行交互时要用异步通信,因为它是单线程的。就简 单性而言,把每一件事情(包括GUI事件和渲染)都放在一个线程里来处理是一个很好的程序模型,因为这样就无需再考虑线程同步这些复杂问题。另一方面,他 也暴露了应用开发中的一个严重问题,单线程环境看起来对用户请求响应迅速,但是当线程忙于处理其它事情时(比如说调用getArticle()),就不能 对用户的鼠标点击和键盘操作做出响应。

如果在这个单线程环境里进行同步通信会发生什么事情呢?同步通信会中断浏览器的执行直至获得通信结果。在等待通信结果的过程中,由于服务器的调用还 没有完成,线程会停止响应用户并保持锁定状态直到调用返回。因为这个原因,当浏览器在等待服务器响应时它不能对用户行为作出响应,所以看起来像是冻结了。 当执行 getArticleWithCache()和backgroundLoad()会有同样的问题,因为它们都是基于getArticle()函数的。由于 下载所有的文章可能会耗费很可观的一段时间,因此对于backgroundLoad()函数来说,浏览器在此段时间内的冻结就是一个很严重的问题——既然 浏览器都已经冻结,当用户正在阅读文章时就不可能首先去执行后台预加载数据,如果这样做连当前的文章都没办法读。

如上所述,既然同步通信在使用中会造成如此严重的问题,JavaScript就把异步通信作为一条基本原则。因此,我们可以基于异步通信改写上面的 程序。 JavaScript要求以一种事件驱动的程序设计方式来写异步通信程序。在很多场合中,你都必须指定一个回调程序,一旦收到通信响应,这个函数就会被调 用。例如,上面定义的getArticleWithCache()可以写成这样:

var cache = {};  function getArticleWithCache ( id, callback ) {      if ( !cache[id] ) {          callback(cache[id]);      } else {          getArticle(id, function( a ){              cache[id] = a;              callback(a);          });      }  }
这个程序也在内部调用了getArticle()函数。然而需要注意的是,为异步通信设计的这版getArticle()函数要接收一个函数作为第 二个参数。当调用这个getArticle()函数时,与从前一样要给服务器发送一个请求,不同的是,现在函数会迅速返回而非等待服务器的响应。这意味 着,当执行权交回给调用程序时,还没有得到服务器的响应。如此一来,线程就可以去执行其它任务直至获得服务器响应,并在此时调用回调函数。一旦得到服务器 响应, getArticle()的第二个参数作为预先定义的回调函数就要被调用,服务器的返回值即为其参数。同样的,getArticleWithCache ()也要做些改变,定义一个回调参数作为其第二个参数。这个回调函数将在被传给getArticle()的回调函数中调用,因而它可以在服务器通信结束后 被执行。

单是上面这些改动你可能已经认为相当复杂了,但是对backgroundLoad()函数做得改动将会更复杂,它也要被改写成可以处理回调函数的形式:

function backgroundLoad ( ids, callback ) {      var i = 0;      function l ( ) {          if ( i < ids.length ) {              getArticleWithCache(ids[i++], function( a ){                  backgroundLoad(a.children, l);              });          } else {              callback();          }      }      l();  }
改动后的backgroundLoad()函数看上去和我们以前的那个函数已经相去甚远,不过他们所实现的功能并无二致。这意味着这两个函数都接受 ID数组作为参数,对于数组里的每个元素都要调用getArticleWithCache(),再应用已经获得子文章ID递归调用 backgroundLoad ()。不过同样是对数组的循环访问,新函数中的就不太好辨认了,以前的程序中是用一个for循环语句完成的。为什么实现同样功能的两套函数是如此的大相径 庭呢?

这个差异源于一个事实:任何函数在遇到有需要同服务器进行通信情况后,都必须立刻返回,例如getArticleWithCache()。除非原来 的函数不在执行当中,否则应当接受服务器响应的回调函数都不能被调用。对于JavaScript,在循环过程中中断程序并在稍后从这个断点继续开始执行程 序是不可能的,例如一个for语句。因此,本例利用递归传递回调函数实现循环结构而非一个传统循环语句。对那些熟悉连续传送风格(CPS)的人来说,这就 是一个 CPS的手动实现,因为不能使用循环语法,所以即便如前面提到的遍历树那么简单的程序也得写得很复杂。与事件驱动程序设计相关的问题是控制流问题:循环和其它控制流表达式可能比较难理解。

这里还有另外一个问题:如果你把一个没有应用异步通信的函数转换为一个使用异步通信的函数,那么重写的函数将需要一个回调函数作为新增参数,这为已 经存在的APIs造成了很大问题,因为内在的改变没有把影响限于内部,而是导致整体混乱的APIs以及API的其它使用者的改变。

造成这些问题目的根本原因是什么呢?没错,正是JavaScript单线程机制导致了这些问题。在单线程里执行异步通信需要事件驱动程序设计和复杂的语句。如果当程序在等待服务器的响应时,有另外一个线程可以来处理用户请求,那么上述复杂技术就不需要了。

转载请注明出处:唯众网络


相关文章 【更多】

唯众网络