啊啊啊啊

基于jquery-1.4.3rc1版本的. 正式版据说过几天就发布, 应该差别不大.

这个系列应该有十章, 本来准备写完了一起发的. 但有几章还不知道会拖到什么时候. 现在完成大概五章的内容了.

会陆续放上来.

这个源码分析系列我不想它成为单纯的翻译注释.除了那种一眼就明白的代码, 其它基本都加了注解.
有时候一句代码的分析可能会关联到一个重要的知识点, 我也尽量在能力范围内把它讲清. 看源码前尽可能的多思考一下,假如是我们实现这个方法,会怎么去做. 再对比jquery的实现就更能加深理解. 另外你也可以对jquery的某些代码保持怀疑, 每次版本更新,都会修复很多bug, 可能你现在怀疑的就是其中一个.

这些源码分析里面的错误和缺陷肯定是有的. 写成文档的目的一是为了梳理自己的知识,再就是希望在和大家的讨论中, 能认识和改正自己的错误.

最后面提供了pdf下载.








------------------------------- 分割线-----------------------------------------------




jquery核心


一  构造jquery.

相对于其它库里传统的构造对象方法. jquery提供了一种截然不同的方法. 它选择创造一个全新的奇异世界.

首先所有的jquery代码被一个自动执行的闭包包裹起来, 只在后面暴露$和jQuery这2个变量给外界 尽量避开变量冲突.

Java代码  收藏代码
  1. (function(window,  undefined){  
  2. …..  
  3. })(window)  


window和undefined都是为了减少变量查找所经过的scope. 当window通过传递给闭包内部之后, 在闭包内部使用它的时候,
可以把它当成一个局部变量, 显然比原先在window scope下查找的时候要快一些.
undefined也是同样的道理, 其实这个undefined并不是javascript数据类型六君子之一的undefined, 而是一个普普通通的变量名.
只是因为没给它传递值. 它的值就是undefined.  undefined并不是javascript的保留字.

然后是一个套子套住jquery的构造方法

Java代码  收藏代码
  1. var jQuery = (function(){   --------------------jq1  
  2.     var jQuery = function( selector, context ){    ---------------jq2  
  3.         ……..  
  4. };  
  5.     return (window.jQuery = window.$ = jQuery);  
  6. })()  



首先定义jq1, 这个jQuery最终在return  (window.jQuery = window.$ = jQuery)的时候会变成
window下面的变量供外界使用.
而jq2供jquery内部执行的时候调用. 最终作为jq1的引用返回.
return (window.jQuery = window.$ = jQuery);这句话等价于

Java代码  收藏代码
  1. window.jQuery = window.$ = jQuery;  
  2. return window. jQuery.  




现在来看看jquery对象是怎么被创建出来的. jquery作为一个独立特行的库,  它产生jquery对象时并不需要用new 操作符..
它宁愿选择这种方式, 比如要产生一个构造函数Man的对象.

Java代码  收藏代码
  1. function Man(name){  
  2.     this.name = name;     
  3. }  
  4.   
  5. function People(name){  
  6.     return new Man(name);  
  7. }  
  8.   
  9. var pe = People("zengtan");  
  10. alert (pe.name);  


同样真正作为jQuery对象的构造方法的并不是
function (selector, context){
}
而是jQuery.fn.init.

Java代码  收藏代码
  1. var jQuery  =  function( selector, context ) {            
  2.         return new jQuery.fn.init( selector, context );  
  3.     }  


jQuery.fn就是jQuery.prototype. 见源码102行.
jQuery.fn = jQuery.prototype = {}

init是挂在jQuery.prototype上的属性.
当jQuery(‘div’)的时候, 实际上转交给了jQuery.fn.init构造函数来生成对象.
当然我们想用new jQuery来生成jquery对象也可以. 跟直接用jQuery()没区别.
因为构造函数一定会返回一个对象.如果显示指定了返回某个对象.就会返回那个对象,
否则才会返回this对象.
好比说, 有只母鸡被你强迫下一个蛋, 它会先看窝里有没有别人的蛋, 如果没有,才会自己努力下一个.
这里显然返回的是jQuery.fn.init的对象.

也许现在你开始回忆制作jquery插件时, 明明是给jQuery.prototype添加方法.
这里返回的又是jQuery.prototype.init的对象.
原来在源码333行,  jQuery.prototype.init.prototype = jQuery. prototype;
现在很容易看明白. 给jQuery.prototype添加方法就等于给jQuery. prototype.init.prototype添加方法了.
JQuery api里的方法大部分都是通过jQuery.prototype扩展上去的, 除此之外. 我们还要给jquery对象加上索引. 给集合添加length属性,让他们更像一个数组里的元素.

搞明白这些, 再来看jQuery. prototype.init这个方法里究竟是怎样生产jquery对象的.
我们可以把jQuery. prototype.init想象成一个火腿肠加工器. 只要你放了正确的原料进去,
它就可以把原料变成火腿肠生产出来.如果你不小心放错了原料.它也会帮你变成火腿肠. 不过只有塑料包装, 里面没有火腿.
当然这个加工器里面的构造是很复杂的, 它需要判断材料种类, 数量等等.
一般这个材料主要为这4种情况
1 dom节点
2 字符串
3 函数
4 数组
5 其他元素

一 jQuery构造方法
jQuery的构造方法会生成一组jquery对象的集合.具体关于init方法的分析, 还是留在选择器部分说吧.

二 jQuery对象访问

jquery构造完对象之后, 会提供一些方法访问这些对象.

1  jQuery.prototype.size 
集合内元素的数量
就是通过this.length得到.

2  jQuery.prototype.get 
按照索引取得集合内某个元素, 返回包装前的原始对象

Java代码  收藏代码
  1. get: function( num ) {  
  2.         return num == null ?   
  3.  //如果参数为null或者undefiend. 注意是==.  
  4.             this.toArray() :  //如果不传参数, 集合内的元素全部转化为一个数组(1)  
  5.             ( num < 0 ? this.slice(num)[ 0 ] : this[ num ] );   
  6. //调用silce方法截取某一个.可以传入负数并且返回的是包装前的对象.  
  7.     }  
  8.   
  9. (1) 看看这里的this.toArray函数.  
  10. toArray: function() {  
  11.         return slice.call( this0 );  
  12.     }  


很简单, 就是让当前jquery对象冒充Array的对象, 调用Array.prototype.slice进行截断. 返回的是一个数组.
至于为什么可以像这样使用对象冒充. 我们抽个地方来好好讨论一下.
其实如果查看v8之类开源引擎的源码就知道(当然也可以在ecma里挣扎一番). 要调用Array原型链上的方法. 通常这个对象满足2个条件就可以了.
1, 本身可以存取属性.
2, length属性不能是只读(可以没有length属性).
由于Array.prototype.slice方法太长了. 我拿Array.prototype.push方法举例. 在V8的src目录下的array.js可以找到这些方法.
比如push

Java代码  收藏代码
  1. function ArrayPush() {  
  2.   var n = ToUint32(this.length);  
  3.  /* 
  4.     对象/数组本身的length. 如果为null或者undefined, 会在ToUint32中转化为0. 所以 
  5.     即使没有length属性,也会给一个默认的0值. 
  6.  */  
  7.   var m = %_ArgumentsLength();    //参数的length.  
  8.   for (var i = 0; i < m; i++) {  
  9.     this[i+n] = %_Arguments(i);   //复制属性  
  10.   }  
  11.   this.length = n + m;            //重设length.  
  12.   return this.length;  
  13. }  


可以看到push操作的核心就是复制属性和重设长度.
jquery对象完全可以满足这2个条件. 同样的道理 一个对象字面量{}也可以.
而string类型的不可以, 因为不能在string上存取属性. function对象虽然可以存取属性, 也有length属性.
不过它的length属性比较特殊, 表示形参的个数, 是一个只读属性, 源码中的this.length = n + m这一句不起作用, 所以function对象也不行. 同理window对象也不行.
上面的slice.call也是这个原理, 虽然slice方法的实现更复杂一点.

明白了这个,我们可以解释很多奇怪的问题.比如:

Java代码  收藏代码
  1. var a = {};  
  2. a[0] = 1;  
  3. a[1] = 2;  
  4. Array.prototype.push(a, 3);  
  5. alert (a.length)   // a没有length属性, 默认的给0值, 所以结果为1 .  


如果在push操作之前添加一句 a.length = 2;
再进行push操作后, a.length就为3了.



3 jQuery.prototype.index 
搜索匹配的元素,并返回相应元素的索引值,从0开始计数。
如果不给 .index() 方法传递参数,那么返回值就是这个jQuery对象集合中第一个元素相对于其同辈元素的位置。
如果参数是一组DOM元素或者jQuery对象,那么返回值就是传递的元素相对于原先集合的位置。
如果参数是一个选择器,那么返回值就是原先元素相对于选择器匹配元素中的位置。如果找不到匹配的元素,则返回-1。
没有什么特别需要解释的, 直接看代码.

Java代码  收藏代码
  1. index: function( elem ) {  
  2.         if ( !elem || typeof elem === "string" ) {     
  3.         //如果没有参数, 或者参数是选择器.  
  4.             return jQuery.inArray( this[0],  
  5.                 elem ? jQuery( elem ) : this.parent().children() );  
  6.         //如果有参数(选择器), 则查找元素本身在这些选择器组成的jq对象集合中的位置  
  7.         //如果没有参数, 查找元素本身在它的兄弟节点之间的位置. this.parent().children()可以取得自己和全部兄弟节点.  
  8. }  
  9.         return jQuery.inArray(  
  10.             //参数是一个对象  
  11.             elem.jquery ? elem[0] : elem, this );  
  12.             //如果是jquery对象, 取得它的原始节点. 再查找在当前集合中的位置     
  13. }  


顾名思义jQuery.inArray就是判断数组里有没有某个元素.当然这里的数组也包括伪数组.
这个方法虽然实现起来很简单, 关于inArray这个名字在jquery的官方论坛却有颇多争议.
很多人认为它应该返回true或者false, 而不是索引的位置.
john resig只是说暂时还不准备修改这个方法.

有些浏览器还不支持Array.prototype.indexOf方法. 所以首先在源码的851行, 有这样一段代码.

Java代码  收藏代码
  1. if ( Array.prototype.indexOf ) {  
  2.     jQuery.inArray = function( elem, array ) {  
  3.         return indexOf.call( array, elem );  
  4.     };  
  5. }  


如果支持Array.prototype.indexOf. 则重写jQuery.inArray, 直接用Array.prototype.indexOf.call(array, elem );
在页面加载的时候就重写这个方法. 也避免了在函数里反复判断造成的浪费.
然后

Java代码  收藏代码
  1. inArray: function( elem, array ) {  
  2.         if ( array.indexOf ) {  
  3. //确认indexOf方法存在.或防止indexOf方法被改写.  
  4.             return array.indexOf( elem );  
  5.         }  
  6.         for ( var i = 0, length = array.length; i < length; i++ ) {  
  7.             //否则遍历数组, 返回正确的索引.  
  8. if ( array[ i ] === elem ) {  
  9.                 return i;  
  10.             }  
  11.         }  
  12.         return -1;  //如果数组里没有这个元素, 返回-1.  
  13.     }  


三 数据缓存

jQuery.data

在实际应用中, 我们经常需要往节点中缓存一些数据. 这些数据往往和dom元素紧密相关. dom节点也是对象, 所以我们可以直接扩展dom节点的属性.
不过肆意污染dom节点是不良少年的行为. 我们需要一种低耦合的方式让dom和缓存数据能够联系起来.

jquery提供了一套非常巧妙的缓存办法.

我们先在jquery内部创建一个cache对象{}, 来保存缓存数据.

然后往需要进行缓存的dom节点上扩展一个值为jQuery.expando的属性, 这里是”jquery” + (new Date).getTime().

接着把每个节点的dom[jQuery.expando]的值都设为一个自增的变量id,保持全局唯一性.
这个id的值就作为cache的key用来关联dom节点和数据.
也就是说cache[id]就取到了这个节点上的所有缓存.

而每个元素的所有缓存都被放到了一个map里面,这样可以同时缓存多个数据.

比如有2个节点dom1和dom2, 它们的缓存数据在cache中的格式应该是这样

Java代码  收藏代码
  1. cache = {  
  2.     dom1[  jQuery.expando ]: {  
  3.         key1: value1,  
  4.         key2: value2  
  5. },  
  6. dom2[ jQuery.expando ] {  
  7.     key3: value3,  
  8.     key4: value4  
  9. }  
  10. }  



jQuery.expando的值等于”jquery”+当前时间, 元素本身具有这种属性而起冲突的情况是微乎其微的.

我们在看源码之前, 先根据上面的原理来自己实现一个简单的缓存系统.以便增强理解.

先把跟data相关的所有代码都封装到一个闭包里,通过返回的接口暴露给外界.
同时为了简便,我们拆分成setData和getData两个方法.

Java代码  收藏代码
  1. <div id="ddd">dddddddd</div>  
  2.   
  3. <script>  
  4.   
  5. var Data = function(){  
  6.     var cache = {};  
  7.     var expando = "zengtan" + +new Date;  
  8.     var uuid = 1;  
  9.       
  10.     var setData = function(elem, key, value){  
  11.             var id = elem[expando];  
  12.             if (!id){   //第一次给元素设置缓存  
  13.                 id = ++uuid;  
  14.                 elem[expando] = id;  
  15.             }  
  16.             if (!cache[id]){   //这个元素第一次进行缓存或者缓存已被清空  
  17.                 cache[id] = {};  
  18.             }  
  19.             cache[id][key] = value;  
  20.     };  
  21.   
  22.     var getData = function(elem, key){  
  23.         var id = elem[expando];  //取得cache里跟dom节点关联的key  
  24.         return cache[id] && cache[id][key] || null;  //如果缓存里没有, 返回null  
  25.     }  
  26.   
  27.     return {  
  28.         setData: setData,  
  29.         getData: getData      
  30.     }  
  31. }()  
  32.   
  33. </script>  
  34.   
  35. var div = document.getElementById("ddd");  
  36. Data.setData(div, "name""zengtan");  
  37. var value = Data.getData(div, "name");  
  38. alert (value) 
posted @ 2012-04-01 16:00  Anjey  阅读(297)  评论(0编辑  收藏  举报