Filed under Web Development

Asynchronous Javascript

本来以为Javascript是有异步机制的,后来发现其实大多数的实现都是使用setTimeout + callback,单单使用callback是不能达到异步的,比如:

	// callback style:
	function renderLotsOfData(data, callback){
		var success = false
		try{
			for(var x in data){
				renderDataitem(data[x]);
			}
			success = true;
		}catch(e){ }
		if(callback){
			callback(success);
		}
	}
	// using callback style
	renderLotsOfData(someDataObj, function(success){
		// handles success or failure
		if(!success){
			promptUserToRecover();
		}
	});
	renderOthers();

renderOthers必须等到callback函数运行完后再触发,所以其实这里是block的,只是callback的使用有“代码段”上的异步的感觉。

dojo.Defferred乍看很神秘,其实只不过是对callback函数的管理,可以有callback函数链的效果,也使复杂的情况下的编码更容易理解。

有些地方使用Node.js的方法process.nextTick来实现异步,文档里面简单的说“This is not a simple alias to setTimeout(fn, 0), it’s much more efficient.” 具体能做到多高效不得而知。而且Node.js虽然很cool,也只能在server端使用。所以感觉js中真正的异步也只有AJAX了,当然,大多数的异步要求也围绕request/response。

最好的方案大概是使用event的subscribe/publish模式了,这在Dojo和Node.js中都大量使用。要在Java中实现这种“异步的Observer模式”需要使用Executor的线程技术,不过由于缺少闭包,这时候模式的使用看上去更像为了弥补语言的缺陷。

Node.js号称任何东西都是异步的,比如I/O的操作,并以此获得Scalability。很多人对此表示怀疑,争论最后变为event和thread这两种模式的比较。Node and Scaling in the Small vs Scaling in the Large这篇指出scaling在大型系统上是综合的考虑,没有哪一种技术是“银弹”,或者说技术的选择已经无关紧要,架构或者设计才是重要的。当然这是冷静的思考,不过Node.js现在版本才0.48,大概没有人会用在大型系统中吧。我觉得event模式优点在于逻辑清晰,而thread代码复杂,难以维护。

感觉语法级别的异步和并发也只有java能做到了,也是久经考验的。Node.js的稳定性则依赖chrome V8引擎的c++代码,多少有点不靠谱,但是javascript语法灵活,这是极大优点。Ruby如果要使用异步还的靠第三方包,不过一般的web应用对这方面也不需要,php啥都没有,照样十分流行。

复杂web应用的非侵入Javascript之路

就跟Javascript是世界上被误解最深的语言一样,HTML也是一种古老的原本简单的技术,现在却被大量甚至过度使用,旧有技术并没有考虑到方方面面,也没有鼓励和抑制某种用法。这样对于复杂应用,旧有的编程模型就得大量的改进。

1. 混合

早期的页面都是逻辑和展示混合在一起的。比如<button id=’sub’ onclick=”submitData();”/>。

2.分开

采用事件绑定。比如在jquery中,$(‘#sub’).bind(‘click’, function(){…}),这样逻辑和显示分开了。这些绑定的代码大多写在页面的onload里面,但这不代表所有的情况。

3.javascript模块化

对于web2.0或者Ajax使用很多的应用,用户感觉他始终在一个页面操作,比如Gmail,左侧的栏是一直在的,不会出现整个页面刷新的情况,多数时候是页面上的某一小块更新内容。

web-dev

比如上面页面右侧区域的内容是根据用户左侧点击操作而动态更新的,这块区域的事件绑定就需要动态的,而不能在整个页面装载时绑定。这样页面各个区域以及整个页面的javascript其实是可以分离开的。

这种web页面结构已经有点走向传统C/S界面开发了,但是html+javascript开发起来难度就大很多。为了避免走向过度复杂,当程序走到这一步需要打住:1)保持简单,避免过度设计(特别是对于小的应用)。只有很少的应用需要做到Gmail的复杂程度。简单是web的本质。2) 重构页面,重构操作模式,不能把C/S的开发思路硬搬到web开发。3)使用RIA之类的胖客户。

模块化为减少全局变量的使用铺平了道路。全局变量不仅指<script>title = ‘All’;</script>这种声明的变量,通过document.getElementbyId也是对全局变量document的访问。这种方式很容易的可以访问页面其他的元素,鼓励了页面元素间的依赖关系。这种细粒度的依赖织成复杂的关系网,当测试复杂程序时会非常困难。

减少全局变量则使另外一个问题突出来:模块间的交互。在有全局变量时候,模块间可以使用这种共享变量来进行通信,现在得另外找道路。event和Observer模式是一个降低模块间耦合度的好方法,jQuery和dojo对这方面都有关注,javascript的函数式语法处理这些得心应手。有些模式比如Front controller也可以达到这种效果。

3.组件

当web应用不可避免的走向B/S模式时,B/S中的组件模型也被复制到html中。典型代表是Dojo,也即Digit widget,还有Ext JS。页面被划分的更细,组件有独立的javascript(事件处理),css。显然这种方式是web开发的理想方式:封装更好,重用性更好。

不过web的优点在于简单和标准,组件对已有编程模型改变太大(侵入式),是一个“框架”而不是“工具”,适合建立复杂的web应用,比如企业应用。一般的应用采用jQuery这种辅助性js库开发更轻量级,更灵活。

Javascript闭包(回调函数)的问题

回调函数网络上一大堆,这里举个实际的例子:

<script src="../javascripts/jquery-1.4.2.js"></script>

<script language="javascript">

  $(document).ready(init);

  function init(){
    console.log("....init");
    var sd = new Date();
    $('.list').each( function(){
      this.onclick= function(){
        console.log("you click,haha");
        process(sd);
      };
    });
  };

  function process(youDate){
    console.log("I got it:" + youDate);
  }

</script>

<div id="nav">
  <div>11</div>
  <div>22</div>
  <div>33</div>
</div>

逻辑很简单:当页面加载时注册回调函数,当点击div时激发回调函数。这里注册和激发是异步的。

注意回调函数可以访问上一级的成员变量sd(或许在FP中称为运行上下文),这个跟java的observer模式中的内部类可以访问容器类的this指针一样,这就是说当你点击div时,原来已经产生的时间变量sd还在,这样打印出来的时间都是旧的时间。这是closure的灵活强大的地方(《JavaScript: The Good Parts》里面说,“使用闭包来进行信息掩藏的方式,它是另一个减少有效全局污染的方法”。真知灼见啊!,否则又要使用大量的全局变量),但是跟Java一样,这里由于引用的关系可能会出现垃圾回收的困难,java用弱引用来处理这个问题。这篇文章Memory leak patterns in JavaScript有描述,里面提到了circular references,大概意思是javascript对象引用了DOM对象,而DOM对象反过来又引用了javascript对象。上面代码改成:

$('.list').click( function(){
  console.log("you click,haha");
  process(s);
});

可能更好点,因为this.onclick处可能隐含了DOM对象到Javascript对象的引用,而在$(‘.list’)处又有javascript obj到dom obj的引用。这个我没有求证,毕竟这种内存泄漏在浏览器上关掉当前页后就应该不存在了,不会引起很明显的问题,知道有这个事就好了。

从“在HTML中嵌入数据”到Dojo的组件模型

有时候web页面会包含一些隐藏数据,或者”非显示”(non-visible)数据。比如Google Reader中的博客文章列表:

table

这里很多的Ajax操作都需要文章的唯一id,而这个id是没有显示在页面的。这种情况应该很常见。

老的方法可能是用类似<a onclick=”process(’243242′);”/>这种方法,不过现在很少用了。这把control逻辑和显示混合到一起了,元素少的时候还不明显,元素多了会有很多的process(‘xxxx’)这样的重复逻辑。(我后来发现jquery binding的时候就可以传递数据,sigh,也算是一种方法吧。)

一般在HTML中嵌入数据的方法为利用HTML Element的class或者attribute。

利用class的例子为:

<div class="entry entry-2"/>
<div class="entry entry-3"/>

利用attribute的例子为:

<div value="10" maximum="100" minimum="0"/>

这里面maximun和minimun都是非标准的属性。

当然也可以使用hidden的element来持有数据,这些方法的缺点在于把数据嵌入到显示中来,导致页面混乱。再者这些非标准的方法使页面丧失语义性,这里详细的讨论了这个问题。HTML5中有一节Embedding custom non-visible data改进了这个问题,这样写:

<div data-ship-id="92432"
data-weapons="laser 2" data-shields="50%"
data-x="30" data-y="10" data-z="90">

个人感觉也其实也很难看,谈不上优雅,比XML差不少。或许HTML本来其实不适合做这种事情,语义网也许是解决的根本之道,唔,扯远了。

这种嵌入数据的需求在Ajax流行下越显突出。web2.0提倡的所有操作在一个页面完成,这样最后页面会很复杂,需要承载更多的数据。

如果从server端返回HTML,这些HTML数据的较之XML和json就复杂多了,因为这样的HTML是数据和显示的混合。特别是在像表格这样的数据比较集中的地方,HTML这种格式不适合javascript对其进行数据的后期处理。(只有在表格这样的地方,这个问题才比较突出)

使用json后,页面的rending又成了一个问题。一般可以用json templete来处理,比如json-template。这样数据是独立出来了,view也独立出来了,不足的地方是缺少一个binding的机制,否则在数据和view间的每次操作都得手工建立对应关系。

Dojo里面的dijit/widget对于这种情况给出了很好的解决方案。widget作为一个对象或者组件,数据可以直接用类似成员变量保存在对象中,不用很怪异的把它嵌入到html中,然后取值又得费劲的操作DOM。

使用组件的概念后,OOP的优点也继承到javascript中来。组件消除了全局变量,对HTML页面进行了隔离,这样规范后就不会出现原来的document.getElementById满天飞的操作。由于不是在总的document之下,一个组件的定义中就不能假设另外的组件的存在,这样,组件之间通过event来耦合,这比getElementById来直接访问是一个很大的进步。

Tagged