?
jQuery是目前最常用的一款开源JS框架。
最新的realease版本是1.7.1,其体积已经达到了229k了。这么多代码肯定不是一蹴而就的,为了更好的学习jQuery的源码、更好的体会一个优秀的开源JS框架的诞生和成长历史。我们从jQery的最初版本开始分析体会,看看jQuery的成长历代记。
?很傻X的看到第一行的代码:
?
? ?? window.undefined = window.undefined;?
???为啥要这么写呢?这样赋值应该和未赋值没有任何区别才对,不过我相信这样一个框架不会做这种很明显多余的动作。(况且我在Ext的源码中也看到过这一句)
这一句是什么意思呢?
原来早期版本的某些浏览器没有全局变量undefined,所以,右边的 window.undefined就会返回undefined,然后把它赋值给window.undefined。
下面就是jQuery的构造器部分的定义:
?
function jQuery( a , c ) { // Shortcut for document ready (because $(document).each() is silly) if ( a && a.constructor == Function && jQuery.fn.ready ) return jQuery( document ).ready( a ); //******************这里省略了很多代码*************************************** }?
?先看第1句 if ( a && a.constructor == Function && jQuery.fn.ready ) ? ?return jQuery( document ).ready( a );
从这句话来看jQuery还是一个普通的function需要使用括号进行执行,我们知道jQuery的第1个参数是选择器参数,可以传递function、字符串等。这一句的意思是首先判断传递进来的选择器是否是一个function,并且后续代码中jQuery.fn.ready已经成功加载。若全满足则jQuery(function(){});等同于jQuery(document).ready(function(){});
这也是那个大家使用jQuery时最最常用的jQuery(function(){})的由来,它是一个简写。
?
我们再看看到了jQuery 1.1时代时这部分的实现方法:
?
if ( jQuery.isFunction( a ) && !a.nodeType && a[0] == undefined ) return new jQuery( document )[jQuery.fn.ready ? "ready" : "load"]( a );?
?
这里的不同是从jQuery 1.1时代开始把一些常用的诸如判断是否为function之类的方法抽象出来了,也越发严谨了一些(判断a不是数组并且a也不是一个Element类型的对象)。需要注意的是这句话中多了一个load哦~至于load和ready的却别后面会详细阐述。
我们再看看jQuery 1.2时代时这部分:?
?
if ( jQuery.isFunction( selector ) ) return jQuery( document )[ jQuery.fn.ready ? "ready" : "load" ]( selector );?
?因为到了1.2开始会先验证DOM等后判断是否为funciton。
... .....
最后到了1.4+时代:
?
if ( jQuery.isFunction( selector ) ) return rootjQuery.ready( selector );?
至于rootjQuery是个什么东东??
?
// All jQuery objects should point back to these rootjQuery = jQuery(document);?
这下知道了,所谓的rootjQuery就是jQuery(document)的引用。为啥要这样用呢?
?因为jQuery从1.5开始引入了jQuery._Deferred,有点类似于缓冲Buffer,后面会进行详解。
所以学习jQuery的源码还是从早期版本看起比较好这样可以比较容易的分析出来设计思路。后期封装的太厉害了,很难梳理出头绪。
//***************我是一个分界*************************
?好吧。让我们回到jQuery年轻的1.01时代:
return jQuery( document ).ready( a );
这里面的ready在后面进行说明,暂时先往下看:
?
// Make sure that a selection was provided a = a || jQuery.context || document;
这个地方就体现出来1.0版本的稚嫩性了,应该先判断参数后进行处理。这里先判断是否为function而后又处理参数a显得逻辑混乱,从jQuery1.2开始这部分就被修改了。
?
?
// Watch for when a jQuery object is passed as the selector if ( a.jquery ) return $( jQuery.merge( a , [] ) );?
如果传递进来的是一个jQuery类型的对象则执行merge:
?
merge : function( first , second ) { var result = []; // Move b over to the new array (this helps to avoid StaticNodeList instances) for ( var k = 0; k < first.length; k++ ) result[k] = first[k]; // Now check for duplicates between a and b and only add the unique items for ( var i = 0; i < second.length; i++ ) { var noCollision = true; // The collision-checking process for ( var j = 0; j < first.length; j++ ){ if ( second[i] == first[j] ) noCollision = false; } // If the item is unique, add it if ( noCollision ) result.push( second[i] ); } return result; }?
?这段代码非常简单就是创建一个数组将first数组的全部内容以及second数组中和first数组不一样的内容抽取出来进行合并,并返回这个新数组的方法。
?
?
// Watch for when a jQuery object is passed at the context if ( c && c.jquery ) return $( c ).find( a ); // If the context is global, return a new object if ( window == this ) return new jQuery( a , c );??
?代码的上半部分说明如果调用jQuery时包含上下文部分则直接执行上下文中过滤a的代码。
下半部分放在这个地方有些不合适,因为是上下文判断所以应该放在最上面才对。
如果调用是window.jQuery(a,c)就相当于直接返回一个jQuery的实例new jQuery(a,c);
?
后面是验证字符串类型的参数
?
?
// Handle HTML strings var m = /^[^<]*(<.+>)[^>]*$/.exec( a ); if ( m ) a = jQuery.clean( [ m[1] ] );?
?首先将字符串a进行处理提取html tag部分内容,但是这里并没有对a的类型进行判断是个问题。
从jQuery 1.1开始增加了对类型的判断
?
// Handle HTML strings if ( typeof a == "string" ) { var m = /^[^<]*(<.+>)[^>]*$/.exec( a ); a = m ? // HANDLE: $(html) -> $(array) jQuery.clean( [ m[1] ] ) : // HANDLE: $(expr) jQuery.find( a , c ); }
?下面是对数组类型的判断
?
// Watch for when an array is passed in this.get( a.constructor == Array || a.length && !a.nodeType && a[0] != undefined && a[0].nodeType ? // Assume that it is an array of DOM Elements jQuery.merge( a , [] ) : // Find the matching elements and save them for later jQuery.find( a , c ) );?
?因为一个对象实例的constructor(构造器)永远指向他的类(class)本身。所以如果a的对象类型是数组则a.constructor==Array。
请注意一点的是逻辑与(&&)的优先级要大于逻辑或(||),所以上面的3元运算符判断部分添加括号后应该是:
a.constructor == Array || (a.length && !a.nodeType && (a[0] != undefined) && a[0].nodeType)
后面的注释就解释的很清楚喽。
?
?
// See if an extra function was provided var fn = arguments[arguments.length - 1]; // If so, execute it in context if ( fn && fn.constructor == Function ) this.each( fn );?
?根据这2句判断当时还可以传递第3个参数,类型为function。若传递此参数就执行jQuery.each。
为啥用this呢?if ( window == this ) return new jQuery( a , c );
jQuery的构造器分析完看下面的代码:
?
// Map over the $ in case of overwrite if ( typeof $ != "undefined" ) jQuery._$ = $; // Map the jQuery namespace to the '$' one var $ = jQuery;?
?上面一句判断是否当前$已经被使用了,比如同时使用了Prototype框架和jQuery框架,且先加载了Prototype。则将Prototype的$指定到jQuery._$中。
将jQuery指定为$ 。这里说明当时jQuery还是很霸道的,总是会覆盖其他框架的$方法。这里说覆盖也不对应该是优先占用$,使用户代码优先被jQuery执行。
下面介绍jQuery的原型。
为啥要介绍原型呢?我们通常使用jQuery的使用都是var a = $("<p>a</p>");或者$(function(){/* do something */});
$( /* ?something */)相当于window.$(/* something */);
还记得上面介绍的if ( window == this ) return new jQuery( a , c );
所以每一个生成的jQuery对象都包含原型中定义的方法。下面上代码:
?
jQuery.fn = jQuery.prototype = { jquery : "$Rev: 509 $", size : function(){... ...}, get : function( num ){... ...}, each : function( fn , args ){... ...}, index : function( obj ){... ...}, attr : function ( key , value , type ) {... ...}, css : function ( key , value ){... ...}, text : function ( e ) {... ...}, wrap : function() {... ...}, append : function() {... ...}, prepend : function() {... ...}, before : function() {... ...}, after : function() {... ...}, end : function() {... ...}, find : function( t ) {... ...}, clone : function( deep ) {... ...}, filter : function( t ) {... ...}, not : function( t ) {... ...}, add : function( t ) {... ...}, is : function( expr ) {... ...}, domManip : function( args , table , dir , fn ) {... ...}, pushStack : function( a , args ) {... ...} }
?
第1个方法是size方法定义如下:
?
size : function() {return this.length;}
?就1句而已,返回当前对象的长度。至于当前对象为啥有length这个属性?是因为整个jQuery结果集就是一个数组还是其他的原因稍后会说明。
?
第2个方法是get ,这个方法的作用和代码的含义需要结合该方法的使用来说明,我们看下jQuery的API:
get()、get(index)
取得所有匹配的 DOM 元素集合。
这是取得所有匹配元素的一种向后兼容的方式(不同于jQuery对象,而实际上是元素数组)。
如果你想要直接操作 DOM 对象而不是 jQuery 对象,这个函数非常有用。
?请看实例:
?
HTML代码如下: <ul> <li id="foo">foo</li> <li id="bar">bar</li> </ul>
?若执行无参数调用 ?alert($('li').get());
则返回: [<li id="foo">, <li id="bar">]
若传递index参数则:($('li').get(0));
返回:<li id="foo">
我们需要注意API原文的这里:‘
?Each jQuery object also masquerades as an array, so we can use the array dereferencing operator to get at the list item instead?
jQuery对象均被伪装成数组的样子所以你可以通过数组下标方式获取元素,上面的写法等价于:alert($('li')[0]);
需要注意的是如果index传递的值为负数则结果为从数组最后一个结果向前查询例如:alert($('li').get(-1))
将返回:<li id="bar">?
?
源码定义如下:?
?
get : function( num ) { // Watch for when an array (of elements) is passed in if ( num && num.constructor == Array ) { // Use a tricky hack to make the jQuery object // look and feel like an array this.length = 0; [ ].push.apply( this , num ); return this; } else{ return num == undefined ? // Return a 'clean' array jQuery.map( this , function( a ) { return a } ) : // Return just the object this[num]; } }
?
?
需要注意的是这里的this,根据前面API的描述这个this已经是被伪装成的数组了,所以如果传递的参数num是一个数组(API中描述这个参数只能是一个索引值,所以这一段代码应该根本不会被执行只能作为容错)。则将参数传递的数组添加(push)到this代表的数组中。
这里可以学习一个小技巧: [ ].push.apply( this , num ); 另一种写法Array.prototype.push.apply(this,num);
Ext用的后一种写法,我感觉后一种更好一些。
到了jQuery 1.3.2后看看get方法的实现:
?
?
get: function( num ) { return num === undefined ? // Return a 'clean' array Array.prototype.slice.call( this ) : // Return just the object this[ num ]; }?
?
这里用的就是我说的方式了。注意这里已经不再判断num是否为一个数组了。(我就说那个判断没用嘛~~~~)
?
PS : Array 的 push() 方法作用:向数组的末尾添加一个或更多元素,并返回新的长度。?
? ? ? ?Array 的 slice()方法的作用:从某个已有的数组返回选定的元素。这个方法可以传递2个参数,如果不传参数,就是返回数组所有元素也就是返回整个数组。
PS2:slice带参数的写法是 Array.prototype.slice.call( this ,0 ) 带1个参数的写法和不写0效果相同。带2个参数就是 Array.prototype.slice.call( this , 0 , 1 )含义就不说了。不知道的去问谷哥哥。
?
我们回头看jQuery 1.01 get方法的最后1句:
?return num == undefined ? jQuery.map( this , function( a ) { return a } ) : this[num];
含义就是判断num参数是否存在存在则返回特定元素即this[num] ,还记得API里说的嘛?”jQuery对象均被伪装成数组的样子所以你可以通过数组下标方式获取元素,上面的写法等价于:alert($('li')[0]);“这回就知道为什么会这样了,因为内部jQuery自己就是这么实现的。
那个返回全部的方法在jQuery 1.3.2时已经变成了 Array.prototype.slice.call( this ) 这种写法肯定比jQuery.map( this , function( a ) { return a } )要高,这里也可以看出随着jQuery的升级对代码的重构会有意识的把封装类的方法替换为js原生方法以提高效率。
虽然我们已经知道Array.prototype.slice.call( this )的含义了,但是jQuery.map( this , function( a ) { return a } )是如何处理的呢?我们看一下map方法的代码:?
?
map : function( elems , fn ) { // ********************这里省略了一些代码********************** for ( var i = 0; i < elems.length; i++ ) { var val = fn( elems[i] , i ); if ( val !== null && val != undefined ) { if ( val.constructor != Array ) { val = [ val ]; } result = jQuery.merge( result , val ); } } return result; }
重点看一下参数为数组时调用的merge,还记得merge嘛?前面提到的,使用的是循环拷贝数组对象的方式。这种方式相当于自己实现了一遍 Array.prototype.slice.call( this )的功能,所以效率必然要低啊。。
这些工具类的merge、map等方法,我们在后面进行专门阐述哦。??
?