浏览器前端编程的面貌自2005年以来已经发生了深刻的变化,这并不简单的意味着出现了大量功能丰富的基础库,使得我们可以更加方便的编写业务代码,更重要的是我们看待前端技术的观念发生了重大转变,明确意识到了如何以前端特有的方式释放程序员的生产力。这里将结合jQuery源码的实现原理,对javascript中涌现出的编程范式和常用技巧作一简单介绍。1.AJAX:状态驻留,异步更新
首先来看一点历史。
A.1995年Netscape公司的BrendanEich开发了javacript语言,这是一种动态(dynamic)、弱类型(weaklytyped)、基于原型(prototype-based)的脚本语言。B.1999年微软IE5发布,其中包含了XMLHTTPActiveX控件。C.2001年微软IE6发布,部分支持DOMlevel1和CSS2标准。D.2002年DouglasCrockford发明JSON格式。
至此,可以说Web2.0所依赖的技术元素已经基本成形,但是并没有立刻在整个业界产生重大的影响。尽管一些“页面异步局部刷新”的技巧在程序员中间秘密的流传,甚至催生了bindows这样庞大臃肿的类库,但总的来说,前端被看作是贫瘠而又肮脏的沼泽地,只有后台技术才是王道。到底还缺少些什么呢?
当我们站在今天的角度去回顾2005年之前的js代码,包括那些当时的牛人所写的代码,可以明显的感受到它们在程序控制力上的孱弱。并不是说2005年之前的js技术本身存在问题,只是它们在概念层面上是一盘散沙,缺乏统一的观念,或者说缺少自己独特的风格,自己的灵魂。当时大多数的人,大多数的技术都试图在模拟传统的面向对象语言,利用传统的面向对象技术,去实现传统的GUI模型的仿制品。
2.模块化:管理名字空间
当大量的代码产生出来以后,我们所需要的最基础的概念就是模块化,也就是对工作进行分解和复用。工作得以分解的关键在于各人独立工作的成果可以集成在一起。这意味着各个模块必须基于一致的底层概念,可以实现交互,也就是说应该基于一套公共代码基,屏蔽底层浏览器的不一致性,并实现统一的抽象层,例如统一的事件管理机制等。比统一代码基更重要的是,各个模块之间必须没有名字冲突。否则,即使两个模块之间没有任何交互,也无法共同工作。
所谓的modulepattern代码如下,它的关键是利用匿名函数限制临时变量的作用域。复制代码代码如下:varfeature=(function(){
//私有变量和函数varprivateThing='secret',publicThing='notsecret',
changePrivateThing=function(){privateThing='supersecret';},
sayPrivateThing=function(){console.log(privateThing);changePrivateThing();};
//返回对外公开的APIreturn{publicThing:publicThing,sayPrivateThing:sayPrivateThing}})();
3.神奇的$:对象提升
这基本对应于如下公式e=$(id)
这绝不仅仅是提供了一个聪明的函数名称缩写,更重要的是在概念层面上建立了文本id与DOMelement之间的一一对应。在未有$之前,id与对应的element之间的距离十分遥远,一般要将element缓存到变量中,例如复制代码代码如下:varea=docuement.getElementById('a');vareb=docuement.getElementById('b');ea.style....但是使用$之后,却随处可见如下的写法复制代码代码如下:$('header_'+id).style...$('body_'+id)....id与element之间的距离似乎被消除了,可以非常紧密的交织在一起。
prototype.js后来扩展了$的含义,复制代码代码如下:function$(){varelements=newArray();for(vari=0;i 这对应于公式:[e,e]=$(id,id) 很遗憾,这一步prototype.js走偏了,这一做法很少有实用的价值。真正将$发扬光大的是jQuery,它的$对应于公式[o]=$(selector)这里有三个增强:A.selector不再是单一的节点定位符,而是复杂的集合选择符B.返回的元素不是原始的DOM节点,而是经过jQuery进一步增强的具有丰富行为的对象,可以启动复杂的函数调用链。C.$返回的包装对象被造型为数组形式,将集合操作自然的整合到调用链中。 当然,以上仅仅是对神奇的$的一个过分简化的描述,它的实际功能要复杂得多.特别是有一个非常常用的直接构造功能.复制代码代码如下:$("
").......
jQuery将根据传入的html文本直接构造出一系列的DOM节点,并将其包装为jQuery对象.这在某种程度上可以看作是对selector的扩展:html内容描述本身就是一种唯一指定.
$(function{})这一功能就实在是让人有些无语了,它表示当document.ready的时候调用此回调函数。真的,$是一个神奇的函数,有任何问题,请$一下。
总结起来,$是从普通的DOM和文本描述世界到具有丰富对象行为的jQuery世界的跃迁通道。跨过了这道门,就来到了理想国。4.无定形的参数:专注表达而不是约束
jQuery早期最主要的卖点就是所谓的链式操作(chain).复制代码代码如下:$('#content')//找到content元素.find('h3')//选择所有后代h3节点.eq(2)//过滤集合,保留第三个元素.html('改变第三个h3的文本').end()//返回上一级的h3集合.eq(0).html('改变第一个h3的文本');
在一般的命令式语言中,我们总需要在重重嵌套循环中过滤数据,实际操作数据的代码与定位数据的代码纠缠在一起.而jQuery采用先构造集合然后再应用函数于集合的方式实现两种逻辑的解耦,实现嵌套结构的线性化.实际上,我们并不需要借助过程化的思想就可以很直观的理解一个集合,例如$('div.myinput:checked')可以看作是一种直接的描述,而不是对过程行为的跟踪.
循环意味着我们的思维处于一种反复回绕的状态,而线性化之后则沿着一个方向直线前进,极大减轻了思维负担,提高了代码的可组合性.为了减少调用链的中断,jQuery发明了一个绝妙的主意:jQuery包装对象本身类似数组(集合).集合可以映射到新的集合,集合可以限制到自己的子集合,调用的发起者是集合,返回结果也是集合,集合可以发生结构上的某种变化但它还是集合,集合是某种概念上的不动点,这是从函数式语言中吸取的设计思想。集合操作是太常见的操作,在java中我们很容易发现大量所谓的封装函数其实就是在封装一些集合遍历操作,而在jQuery中集合操作因为太直白而不需要封装.
链式调用意味着我们始终拥有一个“当前”对象,所有的操作都是针对这一当前对象进行。这对应于如下公式x+=dx调用链的每一步都是对当前对象的增量描述,是针对最终目标的逐步细化过程。Witrix平台中对这一思想也有着广泛的应用。特别是为了实现平台机制与业务代码的融合,平台会提供对象(容器)的缺省内容,而业务代码可以在此基础上进行逐步细化的修正,包括取消缺省的设置等。
话说回来,虽然表面上jQuery的链式调用很简单,内部实现的时候却必须自己多写一层循环,因为编译器并不知道"自动应用于集合中每个元素"这回事.复制代码代码如下:$.fn['someFunc']=function(){returnthis.each(function(){jQuery.someFunc(this,...);}}6.data:统一数据管理
作为一个js库,它必须解决的一个大问题就是js对象与DOM节点之间的状态关联与协同管理问题。有些js库选择以js对象为主,在js对象的成员变量中保存DOM节点指针,访问时总是以js对象为入口点,通过js函数间接操作DOM对象。在这种封装下,DOM节点其实只是作为界面展现的一种底层“汇编”而已。jQuery的选择与Witrix平台类似,都是以HTML自身结构为基础,通过js增强(enhance)DOM节点的功能,将它提升为一个具有复杂行为的扩展对象。这里的思想是非侵入式设计(non-intrusive)和优雅退化机制(gracefuldegradation)。语义结构在基础的HTML层面是完整的,js的作用是增强了交互行为,控制了展现形式。
如果每次我们都通过$('#my')的方式来访问相应的包装对象,那么一些需要长期保持的状态变量保存在什么地方呢?jQuery提供了一个统一的全局数据管理机制。
获取数据:复制代码代码如下:$('#my').data('myAttr')设置数据:复制代码代码如下:$('#my').data('myAttr',3);这一机制自然融合了对HTML5的data属性的处理复制代码代码如下:
"事件沿着对象树传播"这一图景是面向对象界面编程模型的精髓所在。对象的复合构成对界面结构的一个稳定的描述,事件不断在对象树的某个节点发生,并通过冒泡机制向上传播。对象树很自然的成为一个控制结构,我们可以在父节点上监听所有子节点上的事件,而不用明确与每一个子节点建立关联。
如果调用bind之后,新建了另一个li节点,则该节点的click事件不会被监听.
jQuery的delegate机制可以将监听函数注册到父节点上,子节点上触发的事件会根据selector被自动派发到相应的handlerFn上.这样一来现在注册就可以监听未来创建的节点.复制代码代码如下:$('#myList').delegate('li.trigger','click',handlerFn);
最近jQuery1.7中统一了bind,live和delegate机制,天下一统,只有on/off.复制代码代码如下:$('li.trigger').on('click',handlerFn);//相当于bind$('#myList').on('click','li.trigger',handlerFn);//相当于delegate8.动画队列:全局时钟协调
具体的一种实现形式可以是A.对每个动画,将其分装为一个Animation对象,内部分成多个步骤.animation=newAnimation(div,"width",100,200,1000,负责步骤切分的插值函数,动画执行完毕时的回调函数);B.在全局管理器中注册动画对象timerFuncs.add(animation);C.在全局时钟的每一个触发时刻,将每个注册的执行序列推进一步,如果已经结束,则从全局管理器中删除.复制代码代码如下:foreachanimationintimerFuncsif(!animation.doOneStep())timerFuncs.remove(animation)
A.有多个元素要执行类似的动画B.每个元素有多个属性要同时变化C.执行完一个动画之后开始另一个动画jQuery对这些问题的解答可以说是榨尽了js语法表达力的最后一点剩余价值.复制代码代码如下:$('input').animate({left:'+=200px',top:'300'},2000).animate({left:'-=200px',top:20},1000).queue(function(){//这里dequeue将首先执行队列中的后一个函数,因此alert("y")$(this).dequeue();alert('x');}).queue(function(){alert("y");//如果不主动dequeue,队列执行就中断了,不会自动继续下去.$(this).dequeue();});
A.利用jQuery内置的selector机制自然表达对一个集合的处理.B.使用Map表达多个属性变化C.利用微格式表达领域特定的差量概念.'+=200px'表示在现有值的基础上增加200pxD.利用函数调用的顺序自动定义animation执行的顺序:在后面追加到执行队列中的动画自然要等前面的动画完全执行完毕之后再启动.jQuery动画队列的实现细节大概如下所示,
A.animate函数实际是调用queue(function(){执行结束时需要调用dequeue,否则不会驱动下一个方法})queue函数执行时,如果是fx队列,并且当前没有正在运行动画(如果连续调用两次animate,第二次的执行函数将在队列中等待),则会自动触发dequeue操作,驱动队列运行.如果是fx队列,dequeue的时候会自动在队列顶端加入"inprogress"字符串,表示将要执行的是动画.B.针对每一个属性,创建一个jQuery.fx对象。然后调用fx.custom函数(相当于start)来启动动画。C.custom函数中将fx.step函数注册到全局的timerFuncs中,然后试图启动一个全局的timer.timerId=setInterval(fx.tick,fx.interval);D.静态的tick函数中将依次调用各个fx的step函数。step函数中通过easing计算属性的当前值,然后调用fx的update来更新属性。E.fx的step函数中判断如果所有属性变化都已完成,则调用dequeue来驱动下一个方法。
很有意思的是,jQuery的实现代码中明显有很多是接力触发代码:如果需要执行下一个动画就取出执行,如果需要启动timer就启动timer等.这是因为js程序是单线程的,真正的执行路径只有一条,为了保证执行线索不中断,函数们不得不互相帮助一下.可以想见,如果程序内部具有多个执行引擎,甚至无限多的执行引擎,那么程序的面貌就会发生本质性的改变.而在这种情形下,递归相对于循环而言会成为更自然的描述.9.promise模式:因果关系的识别
promise与future模式基本上是一回事,我们先来看一下java中熟悉的future模式.复制代码代码如下:futureResult=doSomething();...realResult=futureResult.get();
发出函数调用仅仅意味着一件事情发生过,并不必然意味着调用者需要了解事情最终的结果.函数立刻返回的只是一个将在未来兑现的承诺(Future类型),实际上也就是某种句柄.句柄被传来传去,中间转手的代码对实际结果是什么,是否已经返回漠不关心.直到一段代码需要依赖调用返回的结果,因此它打开future,查看了一下.如果实际结果已经返回,则future.get()立刻返回实际结果,否则将会阻塞当前的执行路径,直到结果返回为止.此后再调用future.get()总是立刻返回,因为因果关系已经被建立,[结果返回]这一事件必然在此之前发生,不会再发生变化.
future模式一般是外部对象主动查看future的返回值,而promise模式则是由外部对象在promise上注册回调函数.复制代码代码如下:functiongetData(){return$.get('/foo/').done(function(){console.log('FiresaftertheAJAXrequestsucceeds');}).fail(function(){console.log('FiresaftertheAJAXrequestfails');});}functionshowDiv(){vardfd=$.Deferred();$('#foo').fadeIn(1000,dfd.resolve);returndfd.promise();}$.when(getData(),showDiv()).then(function(ajaxResult,ignoreResultFromShowDiv){console.log('FiresafterBOTHshowDiv()ANDtheAJAXrequestsucceed!');//'ajaxResult'istheserver'sresponse});
jQuery引入Deferred结构,根据promise模式对ajax,queue,document.ready等进行了重构,统一了异步执行机制.then(onDone,onFail)将向promise中追加回调函数,如果调用成功完成(resolve),则回调函数onDone将被执行,而如果调用失败(reject),则onFail将被执行.when可以等待在多个promise对象上.promise巧妙的地方是异步执行已经开始之后甚至已经结束之后,仍然可以注册回调函数
someObj.done(callback).sendRequest()vs.someObj.sendRequest().done(callback)
callback函数在发出异步调用之前注册或者在发出异步调用之后注册是完全等价的,这揭示出程序表达永远不是完全精确的,总存在着内在的变化维度.如果能有效利用这一内在的可变性,则可以极大提升并发程序的性能.
promise模式的具体实现很简单.jQuery._Deferred定义了一个函数队列,它的作用有以下几点:
A.保存回调函数。B.在resolve或者reject的时刻把保存着的函数全部执行掉。C.已经执行之后,再增加的函数会被立刻执行。一些专门面向分布式计算或者并行计算的语言会在语言级别内置promise模式,比如E语言.复制代码代码如下:defcarPromise:=carMaker<-produce("Mercedes");deftemperaturePromise:=carPromise<-getEngineTemperature()...when(temperaturePromise)->done(temperature){println(`Thetemperatureofthecarengineis:$temperature`)}catche{println(`Couldnotgetenginetemperature,error:$e`)}在E语言中,<-是eventually运算符,表示最终会执行,但不一定是现在.而普通的car.moveTo(2,3)表示立刻执行得到结果.编译器负责识别所有的promise依赖,并自动实现调度.10.extend:继承不是必须的
曾经有个概念叫做"多重继承",它是继承概念的超级赛亚人版,很遗憾后来被诊断为存在着先天缺陷,以致于出现了一种对于继承概念的解读:继承就是"isa"关系,一个派生对象"isa"很多基类,必然会出现精神分裂,所以多重继承是不好的.复制代码代码如下:classA{public:voidf(){finA}}classB{public:voidf(){finB}}classD:publicA,B{}
面向对象技术发明很久之后,出现了所谓的面向方面编程(AOP),它与OOP不同,是代码结构空间中的定位与修改技术.AOP的眼中只有类与方法,不知道什么叫做意义.AOP也提供了一种类似多重继承的代码重用手段,那就是mixin.对象被看作是可以被打开,然后任意修改的Map,一组成员变量与方法就被直接注射到对象体内,直接改变了它的行为.prototype.js库引入了extend函数,复制代码代码如下:Object.extend=function(destination,source){for(varpropertyinsource){destination[property]=source[property];}returndestination;}
就是Map之间的一个覆盖运算,但很管用,在jQuery库中也得到了延用.这个操作类似于mixin,在jQuery中是代码重用的主要技术手段---没有继承也没什么大不了的.
11.名称映射:一切都是数据
13.browsersniffervs.featuredetection
在具体代码中可以针对不同的浏览器作出不同的处理复制代码代码如下:if($.browser.msie){//dosomething}elseif($.browser.opera){//...}
但是随着浏览器市场的竞争升级,竞争对手之间的互相模仿和伪装导致userAgent一片混乱,加上Chrome的诞生,Safari的崛起,IE也开始加速向标准靠拢,sniffer已经起不到积极的作用.特性检测(featuredetection)作为更细粒度,更具体的检测手段,逐渐成为处理浏览器兼容性的主流方式.复制代码代码如下:jQuery.support={//IEstripsleadingwhitespacewhen.innerHTMLisusedleadingWhitespace:(div.firstChild.nodeType===3),...}
只基于实际看见的,而不是曾经知道的,这样更容易做到兼容未来.
14.Prototypevs.jQuery
prototype.js是一个立意高远的库,它的目标是提供一种新的使用体验,参照Ruby从语言级别对javascript进行改造,并最终真的极大改变了js的面貌。$,extends,each,bind...这些耳熟能详的概念都是prototype.js引入到js领域的.它肆无忌惮的在window全局名字空间中增加各种概念,大有谁先占坑谁有理,舍我其谁的气势.而jQuery则扣扣索索,抱着比较实用化的理念,目标仅仅是writeless,domore而已.
不过等待激进的理想主义者的命运往往都是壮志未酬身先死.当prototype.js标志性的bind函数等被吸收到ECMAScript标准中时,便注定了它的没落.到处修改原生对象的prototype,这是prototype.js的独门秘技,也是它的死穴.特别是当它试图模仿jQuery,通过Element.extend(element)返回增强对象的时候,算是彻底被jQuery给带到沟里去了.prototype.js与jQuery不同,它总是直接修改原生对象的prototype,而浏览器却是充满bug,谎言,历史包袱并夹杂着商业阴谋的领域,在原生对象层面解决问题注定是一场悲剧.性能问题,名字冲突,兼容性问题等等都是一个帮助库的能力所无法解决的.Prototype.js的2.0版本据说要做大的变革,不知是要与历史决裂,放弃兼容性,还是继续挣扎,在夹缝中求生.