jQuery 中的编程范式

浏览器前端编程的面貌自2005年以来已经发生了深刻的变化,这并不简单的意味着出现了大量功能丰富的基础库,使得我们可以更加方便的编写业务代码,更重要的是我们看待前端技术的观念发生了重大转变,明确意识到了如何以前端特有的方式释放程序员的生产力。本文将结合jQuery源码的实现原理,对javascript中涌现出的编程范式和常用技巧作一简单介绍。

1. AJAX: 状态驻留,异步更新
首先来看一点历史。
A. 1995年Netscape公司的Brendan Eich开发了javacript语言,这是一种动态(dynamic)、弱类型(weakly typed)、基于原型(prototype-based)的脚本语言。
B. 1999年微软IE5发布,其中包含了XMLHTTP ActiveX控件。
C. 2001年微软IE6发布,部分支持DOM level 1和CSS 2标准。
D. 2002年Douglas Crockford发明JSON格式。
至此,可以说Web2.0所依赖的技术元素已经基本成形,但是并没有立刻在整个业界产生重大的影响。尽管一些“页面异步局部刷新”的技巧在程序员中间秘密的流传,甚至催生了bindows这样庞大臃肿的类库,但总的来说,前端被看作是贫瘠而又肮脏的沼泽地,只有后台技术才是王道。到底还缺少些什么呢?
当我们站在今天的角度去回顾2005年之前的js代码,包括那些当时的牛人所写的代码,可以明显的感受到它们在程序控制力上的孱弱。并不是说2005年之前的js技术本身存在问题,只是它们在概念层面上是一盘散沙,缺乏统一的观念,或者说缺少自己独特的风格, 自己的灵魂。当时大多数的人,大多数的技术都试图在模拟传统的面向对象语言,利用传统的面向对象技术,去实现传统的GUI模型的仿制品。
2005年是变革的一年,也是创造概念的一年。伴随着Google一系列让人耳目一新的交互式应用的发布,Jesse James Garrett的一篇文章《Ajax: A New Approach to Web Applications》被广为传播。Ajax这一前端特有的概念迅速将众多分散的实践统一在同一口号之下,引发了Web编程范式的转换。所谓名不正则言不顺,这下无名群众可找到组织了。在未有Ajax之前,人们早已认识到了B/S架构的本质特征在于浏览器和服务器的状态空间是分离的,但是一般的解决方案都是隐藏这一区分,将前台状态同步到后台,由后台统一进行逻辑处理,例如ASP.NET。因为缺乏成熟的设计模式支持前台状态驻留,在换页的时候,已经装载的js对象将被迫被丢弃,这样谁还能指望它去完成什么复杂的工作吗?
Ajax明确提出界面是局部刷新的,前台驻留了状态,这就促成了一种需要:需要js对象在前台存在更长的时间。这也就意味着需要将这些对象和功能有效的管理起来,意味着更复杂的代码组织技术,意味着对模块化,对公共代码基的渴求。
jQuery现有的代码中真正与Ajax相关(使用XMLHTTP控件异步访问后台返回数据)的部分其实很少,但是如果没有Ajax, jQuery作为公共代码基也就缺乏存在的理由。

2. 模块化:管理名字空间
当大量的代码产生出来以后,我们所需要的最基础的概念就是模块化,也就是对工作进行分解和复用。工作得以分解的关键在于各人独立工作的成果可以集成在一起。这意味着各个模块必须基于一致的底层概念,可以实现交互,也就是说应该基于一套公共代码基,屏蔽底层浏览器的不一致性,并实现统一的抽象层,例如统一的事件管理机制等。比统一代码基更重要的是,各个模块之间必须没有名字冲突。否则,即使两个模块之间没有任何交互,也无法共同工作。
jQuery目前鼓吹的主要卖点之一就是对名字空间的良好控制。这甚至比提供更多更完善的功能点都重要的多。良好的模块化允许我们复用任何来源的代码,所有人的工作得以积累叠加。而功能实现仅仅是一时的工作量的问题。jQuery使用module pattern的一个变种来减少对全局名字空间的影响,仅仅在window对象上增加了一个jQuery对象(也就是$函数)。
所谓的module pattern代码如下,它的关键是利用匿名函数限制临时变量的作用域。

01 var feature =(function() {
02  
03 // 私有变量和函数
04 var privateThing = ’secret’,
05     publicThing = ’not secret’,
06  
07     changePrivateThing = function() {
08         privateThing = ’super secret’;
09     },
10  
11     sayPrivateThing = function() {
12         console.log(privateThing);
13         changePrivateThing();
14     };
15  
16 // 返回对外公开的API
17 return {
18     publicThing : publicThing,
19     sayPrivateThing :  sayPrivateThing
20 }
21 })();

 

js本身缺乏包结构,不过经过多年的尝试之后业内已经逐渐统一了对包加载的认识,形成了RequireJs库这样得到一定共识的解决方案。jQuery可以与RequireJS库良好的集成在一起, 实现更完善的模块依赖管理。http://requirejs.org/docs/jquery.html

1 require(["jquery", "jquery.my"], function() {
2     //当jquery.js和jquery.my.js都成功装载之后执行
3     $(function(){
4       $(‘#my’).myFunc();
5     });
6   });

 

通过以下函数调用来定义模块my/shirt, 它依赖于my/cart和my/inventory模块,

01 require.def(“my/shirt”,
02   ["my/cart", "my/inventory"],
03   function(cart, inventory) {
04       // 这里使用module pattern来返回my/shirt模块对外暴露的API
05       return {
06           color: ”blue”,
07           size: ”large”
08           addToCart: function() {
09               // decrement是my/inventory对外暴露的API
10               inventory.decrement(this);
11               cart.add(this);
12           }
13       }
14   }
15 );

 

3. 神奇的$:对象提升
当你第一眼看到$函数的时候,你想到了什么?传统的编程理论总是告诉我们函数命名应该准确,应该清晰无误的表达作者的意图,甚至声称长名字要优于短名字,因为减少了出现歧义的可能性。但是,$是什么?乱码?它所传递的信息实在是太隐晦,太暧昧了。$是由prototype.js库发明的,它真的是一个神奇的函数,因为它可以将一个原始的DOM节点提升(enhance)为一个具有复杂行为的对象。在prototype.js最初的实现中,$函数的定义为

1 var $ = function (id) {
2   return ”string” == typeof id ? document.getElementById(id) : id;
3 };

 

这基本对应于如下公式

1 e = $(id)

 

这绝不仅仅是提供了一个聪明的函数名称缩写,更重要的是在概念层面上建立了文本id与DOM element之间的一一对应。在未有$之前,id与对应的element之间的距离十分遥远,一般要将element缓存到变量中,例如

1 var ea = docuement.getElementById(‘a’);
2 var eb = docuement.getElementById(‘b’);
3 ea.style….

 

但是使用$之后,却随处可见如下的写法

1 $(‘header_’+id).style…
2 $(‘body_’+id)….

 

id与element之间的距离似乎被消除了,可以非常紧密的交织在一起。
prototype.js后来扩展了$的含义,

01 function $() {
02   var elements = new Array();
03  
04   for (var i = 0; i < arguments.length; i++) {
05       var element = arguments[i];
06       if (typeof element == ’string’)
07         element = document.getElementById(element);
08  
09       if (arguments.length == 1)
10         return element;
11  
12       elements.push(element);
13   }
14  
15   return elements;
16 }

 

这对应于公式

1 [e,e] = $(id,id)

 

很遗憾,这一步prototype.js走偏了,这一做法很少有实用的价值。
真正将$发扬光大的是jQuery, 它的$对应于公式

1 [o] = $(selector)

 

这里有三个增强
A. selector不再是单一的节点定位符,而是复杂的集合选择符
B. 返回的元素不是原始的DOM节点,而是经过jQuery进一步增强的具有丰富行为的对象,可以启动复杂的函数调用链。
C. $返回的包装对象被造型为数组形式,将集合操作自然的整合到调用链中。

当然,以上仅仅是对神奇的$的一个过分简化的描述,它的实际功能要复杂得多. 特别是有一个非常常用的直接构造功能

1 $(“<table><tbody><tr><td>…</td></tr></tbody></table>”)….

 

jQuery将根据传入的html文本直接构造出一系列的DOM节点,并将其包装为jQuery对象. 这在某种程度上可以看作是对selector的扩展: html内容描述本身就是一种唯一指定.
$(function{})这一功能就实在是让人有些无语了, 它表示当document.ready的时候调用此回调函数。真的,$是一个神奇的函数, 有任何问题,请$一下。
总结起来, $是从普通的DOM和文本描述世界到具有丰富对象行为的jQuery世界的跃迁通道。跨过了这道门,就来到了理想国。

4. 无定形的参数:专注表达而不是约束
弱类型语言既然头上顶着个”弱”字, 总难免让人有些先天不足的感觉. 在程序中缺乏类型约束, 是否真的是一种重大的缺憾? 在传统的强类型语言中, 函数参数的类型,个数等都是由编译器负责检查的约束条件, 但这些约束仍然是远远不够的. 一般应用程序中为了加强约束, 总会增加大量防御性代码, 例如在C++中我们常用ASSERT, 而在java中也经常需要判断参数值的范围

1 if (index < 0 || index >= size)
2     throw new IndexOutOfBoundsException(
3         ”Index: ”+index+”, Size: ”+size);

 

很显然, 这些代码将导致程序中存在大量无功能的执行路径, 即我们做了大量判断, 代码执行到某个点, 系统抛出异常, 大喊此路不通. 如果我们换一个思路, 既然已经做了某种判断,能否利用这些判断的结果来做些什么呢? javascript是一种弱类型的语言,它是无法自动约束参数类型的, 那如果顺势而行,进一步弱化参数的形态, 将”弱”推进到一种极致, 在弱无可弱的时候, weak会不会成为标志性的特点?
看一下jQuery中的事件绑定函数bind,
A. 一次绑定一个事件 $(“#my”).bind(“mouseover”, function(){});
B. 一次绑定多个事件 $(“#my”).bind(“mouseover mouseout”,function(){})
C. 换一个形式, 同样绑定多个事件
$(“#my”).bind({mouseover:function(){}, mouseout:function(){});
D. 想给事件监听器传点参数
$(‘#my’).bind(‘click’, {foo: “xxxx”}, function(event) { event.data.foo..})
E. 想给事件监听器分个组
$(“#my”).bind(“click.myGroup″, function(){});
F. 这个函数为什么还没有疯掉???

就算是类型不确定, 在固定位置上的参数的意义总要是确定的吧? 退一万步来说, 就算是参数位置不重要了,函数本身的意义应该是确定的吧? 但这是什么

1 //取值
2 value = o.val(),
3 //设置值
4 o.val(3)

 

一个函数怎么可以这样过分, 怎么能根据传入参数的类型和个数不同而行为不同呢? 看不顺眼是不是? 可这就是俺们的价值观. 既然不能防止, 那就故意允许. 虽然形式多变, 却无一句废话. 缺少约束, 不妨碍表达(我不是出来吓人的).

5. 链式操作: 线性化的逐步细化
jQuery早期最主要的卖点就是所谓的链式操作(chain).

1 $(‘#content’) // 找到content元素
2     .find(‘h3′) // 选择所有后代h3节点
3     .eq(2)      // 过滤集合, 保留第三个元素
4         .html(‘改变第三个h3的文本’)
5     .end()      // 返回上一级的h3集合
6     .eq(0)
7         .html(‘改变第一个h3的文本’);

 

在一般的命令式语言中, 我们总需要在重重嵌套循环中过滤数据, 实际操作数据的代码与定位数据的代码纠缠在一起. 而jQuery采用先构造集合然后再应用函数于集合的方式实现两种逻辑的解耦, 实现嵌套结构的线性化. 实际上, 我们并不需要借助过程化的思想就可以很直观的理解一个集合, 例如 $(‘div.my input:checked’)可以看作是一种直接的描述,而不是对过程行为的跟踪.
循环意味着我们的思维处于一种反复回绕的状态, 而线性化之后则沿着一个方向直线前进, 极大减轻了思维负担, 提高了代码的可组合性. 为了减少调用链的中断, jQuery发明了一个绝妙的主意: jQuery包装对象本身类似数组(集合). 集合可以映射到新的集合, 集合可以限制到自己的子集合,调用的发起者是集合,返回结果也是集合,集合可以发生结构上的某种变化但它还是集合, 集合是某种概念上的不动点,这是从函数式语言中吸取的设计思想。集合操作是太常见的操作, 在java中我们很容易发现大量所谓的封装函数其实就是在封装一些集合遍历操作, 而在jQuery中集合操作因为太直白而不需要封装.
链式调用意味着我们始终拥有一个“当前”对象,所有的操作都是针对这一当前对象进行。这对应于如下公式

1 x += dx

 

调用链的每一步都是对当前对象的增量描述,是针对最终目标的逐步细化过程。Witrix平台中对这一思想也有着广泛的应用。特别是为了实现平台机制与业务代码的融合,平台会提供对象(容器)的缺省内容,而业务代码可以在此基础上进行逐步细化的修正,包括取消缺省的设置等。
话说回来, 虽然表面上jQuery的链式调用很简单, 内部实现的时候却必须自己多写一层循环, 因为编译器并不知道”自动应用于集合中每个元素”这回事.

1 $.fn['someFunc'] = function(){
2     return this.each(function(){
3       jQuery.someFunc(this,…);
4     }
5   }

 

6. data: 统一数据管理
作为一个js库,它必须解决的一个大问题就是js对象与DOM节点之间的状态关联与协同管理问题。有些js库选择以js对象为主,在js对象的成员变量中保存DOM节点指针,访问时总是以js对象为入口点,通过js函数间接操作DOM对象。在这种封装下,DOM节点其实只是作为界面展现的一种底层“汇编” 而已。jQuery的选择与Witrix平台类似,都是以HTML自身结构为基础,通过js增强(enhance)DOM节点的功能,将它提升为一个具有复杂行为的扩展对象。这里的思想是非侵入式设计(non-intrusive)和优雅退化机制(graceful degradation)。语义结构在基础的HTML层面是完整的,js的作用是增强了交互行为,控制了展现形式。
如果每次我们都通过$(‘#my’)的方式来访问相应的包装对象,那么一些需要长期保持的状态变量保存在什么地方呢?jQuery提供了一个统一的全局数据管理机制。

1 //获取数据
2 $(‘#my’).data(‘myAttr’)  
3 //设置数据
4 $(‘#my’).data(‘myAttr’,3);

 

这一机制自然融合了对HTML5的data属性的处理

1 <input id=”my” data-my-attr=”4″ … />

 

通过 $(‘#my’).data(‘myAttr’)将可以读取到HTML中设置的数据。

第一次访问data时,jQuery将为DOM节点分配一个唯一的uuid, 然后设置在DOM节点的一个特定的expando属性上, jQuery保证这个uuid在本页面中不重复。

1 elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];

 

以上代码可以同时处理DOM节点和纯js对象的情况。如果是js对象,则data直接放置在js对象自身中,而如果是DOM节点,则通过cache统一管理。
因为所有的数据都是通过data机制统一管理的,特别是包括所有事件监听函数(data.events),因此jQuery可以安全的实现资源管理。在 clone节点的时候,可以自动clone其相关的事件监听函数。而当DOM节点的内容被替换或者DOM节点被销毁的时候,jQuery也可以自动解除事件监听函数, 并安全的释放相关的js数据。

7. event:统一事件模型
“事件沿着对象树传播”这一图景是面向对象界面编程模型的精髓所在。对象的复合构成对界面结构的一个稳定的描述,事件不断在对象树的某个节点发生,并通过冒泡机制向上传播。对象树很自然的成为一个控制结构,我们可以在父节点上监听所有子节点上的事件,而不用明确与每一个子节点建立关联。
jQuery除了为不同浏览器的事件模型建立了统一抽象之外,主要做了如下增强:
A. 增加了自定制事件(custom)机制. 事件的传播机制与事件内容本身原则上是无关的, 因此自定制事件完全可以和浏览器内置事件通过同一条处理路径, 采用同样的监听方式. 使用自定制事件可以增强代码的内聚性, 减少代码耦合. 例如如果没有自定制事件, 关联代码往往需要直接操作相关的对象

1 $(‘.switch, .clapper’).click(function() {
2     var $light = $(this).parent().find(‘.lightbulb’);
3     if ($light.hasClass(‘on’)) {
4         $light.removeClass(‘on’).addClass(‘off’);
5     } else {
6         $light.removeClass(‘off’).addClass(‘on’);
7     }
8   });

 

而如果使用自定制事件,则表达的语义更加内敛明确,

1 $(‘.switch, .clapper’).click(function() {
2   $(this).parent().find(‘.lightbulb’).trigger(‘changeState’);
3 });

 

B. 增加了对动态创建节点的事件监听. bind函数只能将监听函数注册到已经存在的DOM节点上. 例如

1 $(‘li.trigger’).bind(‘click’,function(){}}

 

如果调用bind之后,新建了另一个li节点,则该节点的click事件不会被监听.
jQuery的delegate机制可以将监听函数注册到父节点上, 子节点上触发的事件会根据selector被自动派发到相应的handlerFn上. 这样一来现在注册就可以监听未来创建的节点.

1 $(‘#myList’).delegate(‘li.trigger’, ’click’, handlerFn);

 

最近jQuery1.7中统一了bind, live和delegate机制, 天下一统, 只有on/off.

1 $(‘li.trigger’).on(‘click’, handlerFn);  // 相当于bind
2 $(‘#myList’).on(‘click’, ’li.trigger’, handlerFn);  // 相当于delegate

 

 
8. 动画队列:全局时钟协调
抛开jQuery的实现不谈, 先考虑一下如果我们要实现界面上的动画效果, 到底需要做些什么? 比如我们希望将一个div的宽度在1秒钟之内从100px增加到200px. 很容易想见, 在一段时间内我们需要不时的去调整一下div的宽度, [同时]我们还需要执行其他代码. 与一般的函数调用不同的是, 发出动画指令之后, 我们不能期待立刻得到想要的结果, 而且我们不能原地等待结果的到来. 动画的复杂性就在于:一次性表达之后要在一段时间内执行,而且有多条逻辑上的执行路径要同时展开, 如何协调?
伟大的艾萨克.牛顿爵士在《自然哲学的数学原理》中写道:”绝对的、真正的和数学的时间自身在流逝着”. 所有的事件可以在时间轴上对齐, 这就是它们内在的协调性. 因此为了从步骤A1执行到A5, 同时将步骤B1执行到B5, 我们只需要在t1时刻执行[A1, B1], 在t2时刻执行[A2,B2], 依此类推.

1 t1 | t2 | t3 | t4 | t5 …
2 A1 | A2 | A3 | A4 | A5 …
3 B1 | B2 | B3 | B4 | B5 …

 

具体的一种实现形式可以是
A. 对每个动画, 将其分装为一个Animation对象, 内部分成多个步骤.

1 animation = new Animation(div,”width”,100,200,1000,
2 //负责步骤切分的插值函数,动画执行完毕时的回调函数);

 

B. 在全局管理器中注册动画对象

1 timerFuncs.add(animation);

 

C. 在全局时钟的每一个触发时刻, 将每个注册的执行序列推进一步, 如果已经结束, 则从全局管理器中删除.

1 for each animation in timerFuncs
2    if(!animation.doOneStep())
3       timerFuncs.remove(animation)

 

解决了原理问题,再来看看表达问题, 怎样设计接口函数才能够以最紧凑形式表达我们的意图? 我们经常需要面临的实际问题:
A. 有多个元素要执行类似的动画
B. 每个元素有多个属性要同时变化
C. 执行完一个动画之后开始另一个动画
jQuery对这些问题的解答可以说是榨尽了js语法表达力的最后一点剩余价值.

01 $(‘input’)
02   .animate({left:’+=200px’,top:’300′},2000)
03   .animate({left:’-=200px’,top:20},1000)
04   .queue(function(){
05     // 这里dequeue将首先执行队列中的后一个函数,因此alert(“y”)
06     $(this).dequeue();
07     alert(‘x’);
08    })
09   .queue(function(){
10      alert(“y”);

你可能感兴趣的:(jquery)