基于jquery-1.4.3rc1版本的. 正式版据说过几天就发布, 应该差别不大.
这个系列应该有十章, 本来准备写完了一起发的. 但有几章还不知道会拖到什么时候. 现在完成大概五章的内容了. 会陆续放上来. 这个源码分析系列我不想它成为单纯的翻译注释.除了那种一眼就明白的代码, 其它基本都加了注解. 有时候一句代码的分析可能会关联到一个重要的知识点, 我也尽量在能力范围内把它讲清. 看源码前尽可能的多思考一下,假如是我们实现这个方法,会怎么去做. 再对比jquery的实现就更能加深理解. 另外你也可以对jquery的某些代码保持怀疑, 每次版本更新,都会修复很多bug, 可能你现在怀疑的就是其中一个. 这些源码分析里面的错误和缺陷肯定是有的. 写成文档的目的一是为了梳理自己的知识,再就是希望在和大家的讨论中, 能认识和改正自己的错误. 最后面提供了pdf下载. ------------------------------- 分割线----------------------------------------------- jquery核心 一 构造jquery. 相对于其它库里传统的构造对象方法. jquery提供了一种截然不同的方法. 它选择创造一个全新的奇异世界. 首先所有的jquery代码被一个自动执行的闭包包裹起来, 只在后面暴露$和jQuery这2个变量给外界 尽量避开变量冲突.
window和undefined都是为了减少变量查找所经过的scope. 当window通过传递给闭包内部之后, 在闭包内部使用它的时候, 可以把它当成一个局部变量, 显然比原先在window scope下查找的时候要快一些. undefined也是同样的道理, 其实这个undefined并不是javascript数据类型六君子之一的undefined, 而是一个普普通通的变量名. 只是因为没给它传递值. 它的值就是undefined. undefined并不是javascript的保留字. 然后是一个套子套住jquery的构造方法
首先定义jq1, 这个jQuery最终在return (window.jQuery = window.$ = jQuery)的时候会变成 window下面的变量供外界使用. 而jq2供jquery内部执行的时候调用. 最终作为jq1的引用返回. return (window.jQuery = window.$ = jQuery);这句话等价于
现在来看看jquery对象是怎么被创建出来的. jquery作为一个独立特行的库, 它产生jquery对象时并不需要用new 操作符.. 它宁愿选择这种方式, 比如要产生一个构造函数Man的对象.
同样真正作为jQuery对象的构造方法的并不是 function (selector, context){ } 而是jQuery.fn.init.
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 按照索引取得集合内某个元素, 返回包装前的原始对象
很简单, 就是让当前jquery对象冒充Array的对象, 调用Array.prototype.slice进行截断. 返回的是一个数组. 至于为什么可以像这样使用对象冒充. 我们抽个地方来好好讨论一下. 其实如果查看v8之类开源引擎的源码就知道(当然也可以在ecma里挣扎一番). 要调用Array原型链上的方法. 通常这个对象满足2个条件就可以了. 1, 本身可以存取属性. 2, length属性不能是只读(可以没有length属性). 由于Array.prototype.slice方法太长了. 我拿Array.prototype.push方法举例. 在V8的src目录下的array.js可以找到这些方法. 比如push
可以看到push操作的核心就是复制属性和重设长度. jquery对象完全可以满足这2个条件. 同样的道理 一个对象字面量{}也可以. 而string类型的不可以, 因为不能在string上存取属性. function对象虽然可以存取属性, 也有length属性. 不过它的length属性比较特殊, 表示形参的个数, 是一个只读属性, 源码中的this.length = n + m这一句不起作用, 所以function对象也不行. 同理window对象也不行. 上面的slice.call也是这个原理, 虽然slice方法的实现更复杂一点. 明白了这个,我们可以解释很多奇怪的问题.比如:
如果在push操作之前添加一句 a.length = 2; 再进行push操作后, a.length就为3了. 3 jQuery.prototype.index 搜索匹配的元素,并返回相应元素的索引值,从0开始计数。 如果不给 .index() 方法传递参数,那么返回值就是这个jQuery对象集合中第一个元素相对于其同辈元素的位置。 如果参数是一组DOM元素或者jQuery对象,那么返回值就是传递的元素相对于原先集合的位置。 如果参数是一个选择器,那么返回值就是原先元素相对于选择器匹配元素中的位置。如果找不到匹配的元素,则返回-1。 没有什么特别需要解释的, 直接看代码.
顾名思义jQuery.inArray就是判断数组里有没有某个元素.当然这里的数组也包括伪数组. 这个方法虽然实现起来很简单, 关于inArray这个名字在jquery的官方论坛却有颇多争议. 很多人认为它应该返回true或者false, 而不是索引的位置. john resig只是说暂时还不准备修改这个方法. 有些浏览器还不支持Array.prototype.indexOf方法. 所以首先在源码的851行, 有这样一段代码.
如果支持Array.prototype.indexOf. 则重写jQuery.inArray, 直接用Array.prototype.indexOf.call(array, elem ); 在页面加载的时候就重写这个方法. 也避免了在函数里反复判断造成的浪费. 然后
三 数据缓存 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中的格式应该是这样
jQuery.expando的值等于”jquery”+当前时间, 元素本身具有这种属性而起冲突的情况是微乎其微的. 我们在看源码之前, 先根据上面的原理来自己实现一个简单的缓存系统.以便增强理解. 先把跟data相关的所有代码都封装到一个闭包里,通过返回的接口暴露给外界. 同时为了简便,我们拆分成setData和getData两个方法.
看看源码实现. 首先声明一些特殊的节点, 在它们身上存属性的时候可能会抛出异常.
这个对象里的数据用在acceptData方法中, 跟1.42版本相比, 这里多了对什么flash的object的特殊处理. 总之acceptData方法就是判断节点能否添加缓存. 看具体的jQuery.data
从新版本的源码里可以看到, 1.42版本中data方法的几个缺点已经被解决了. 当然我们用jquery缓存系统的时候, 一般调用的是prototype方法, prototype方法除了调用上面的静态方法之外. 还加入了对节点上自定义事件的处理, 留在event部分再讲. 当然, 我们还需要删除缓存的方法. 现在看看removeData的代码
四 队列控制 队列控制也是jquery中很有特点的一个功能. 可以用来管理动画或者事件等的执行顺序. queue和dequeue主要为动画服务. 比如在jquery的动画里, 因为javascript的单线程异步机制, 如果要管理一批动画的执行顺序, 而不是让它们一起在屏幕上飞舞. 一般我们是一个一个的把下个动画写在上个动画的回调函数中, 意味着如果要让十个动画按次序执行. 至少要写9个回调函数. 好吧我承认我做过这样的事, 直接导致我的视力从5.2变为5.1. 现在有了队列机制, 可以把动画都放到队列中依次执行.究竟怎样把动画填充进队列.用的是queue方法. 不过queue非常先进的是.把动画push进队列之后,还会自动去执行队列里的第一个函数. 队列机制里面的另外一个重要方法是dequeue, 意为取出队列的第一个函数并执行.此时队列里面的函数个数变为N-1. 看个例子.比如我要让2个div以动画效果交替隐藏和显示.同时只能有一个div在进行动画.
首先我们需要一个载体, 来保存这个队列, 这里选择了document. 其实选什么节点都一样, 保存队列其实也是一个jQuery.data操作. 然后queue的参数是一个数组. 里面的函数就是队列依次执行的函数. 前面讲到, queue方法会自动把队列里的第一个函数取出来执行. 意味着这些代码写完后, div1已经开始渐渐隐藏了. 隐藏完毕后, 如果要让后面的动画继续执行, 还要用$(document).dequeue()继续取出并执行现在队列里的第一个函数. 当然这个操作是放在第一个动画的回调函数里, 以此类推, 第二个.dequeue()要放在第二个动画的回调函数里. 我们看到这里没有用$(document).dequeue(). 因为这句代码太长. 注意队列函数里有一个参数fn, fn是dequeue方法内部传给此函数的, 就是$(document).dequeue(). 在看源码之前, 先自己来想想这个功能应该怎么实现. 首先我们需要一个数组, 里面存放那些动画. 然后需要2个方法, set和get.可以存取动画. 可能我们还需要一个变量来模拟线程锁, 保证队列里的函数不会同时被执行. 最后我们要把这个数组存入dom的缓存中, 方便随时存取和删除. 看源码, 先是prototype方法.
“inprogress”进程锁是这样工作的: 如果是dequeue操作, 去掉锁, 执行队列里的函数, 同时给队列加上锁. 如果是queue操作, 要看锁的状态, 如果被锁上了, 就只执行队列的添加操作. 不再调用dequeue. 其实dequeue和queue都可以执行队列里的第一个函数.queue操作添加完队列之后, 会调用dequeue方法去执行函数. 但是用dequeue执行函数的时候, 这时候如果又用queue触发dequeue的话, 很可能同时有2个函数在执行. 队列就失去一大半意义了(还是可以保证顺序, 但是2个动画会同时执行). 不过这个锁只能保证在dequeue的时候, 不被queue操作意外的破坏队列. 如果人为的同时用2个dequeue, 还是会破坏动画效果的. 所以要把fn写在回调函数里. 清空队列
原理上面已经提到过了, 就是这一句
用一个空数组代替了原来的队列. 五 多库共存 jQuery.noConflict 将变量$的控制权让渡给上一个实现它的那个库. 可能很多人都被多库共存时的$变量问题困扰过. 比如你先引入了prototype库,然后又引入了jquery.因为jquery的$会覆盖prototype的$. 现在想调用prototype的$怎么办呢, noConflict方法就派上用场了. Jquery代码里最开始就有一句_$ = window.$ , 在加载的时候就用_$来引用原来的$ (比如现在就是prototype库里的$).
继承和拷贝 jQuery.prototype.extend和jQuery.extend 扩展 jQuery 元素集来提供新的方法, 或者把一个对象的属性拷贝到另外一个对象上. 通常javascript的继承实现有4种方式. 1 构造继承 2 prototype继承 3 属性拷贝 4 实例继承(这个有点特殊, 主要用于继承内置对象) 这4种继承都有各自的优点和缺陷. 构造继承查找属性快.但无法继承原型链上的方法,而且每次都要为属性复制一份新的副本进行储存 原型继承在性能方面好一些,可以共享原型链.但查找属性慢,因为可能要遍历N条原型链才能找到某个属性.而且原型链太多,会使得结构越加混乱.并且会丢失对象的constructor属性 (对象的constructor总是指向原型链最上层的构造器) 属性拷贝非常灵活,但明显效率偏低. 而且仅仅只是模拟继承. 实例继承主要用来继承Array, Date等内置对象, 用的较少. jquery里采用的是属性拷贝.其实用哪种继承是个见仁见智的问题. 也有其它一些库就是用的原型继承. 前面也可以注意到, jquery只在构造方法或者原型链上定义了少量的核心方法. 其它功能块都是通过extend函数拷贝上去.按需定制. 好比开发商交给我们房子的时候, 只安装了水电暖气等基本设施.电视机洗衣机显然是自己去买比较好. 属性拷贝原理就是遍历第二个对象, 然后分别把键和值填入第一个对象中.类似于
不幸的是jquery里面的做法复杂的多,不仅可以把多个参数的属性都复制到一个对象中,还能实现深度继承. 深度继承是指如果被继承对象和继承对象都有同名属性a, 而且这个属性的值分别是一个对象b和c.会把b和c的属性都合并到一个新的对象里, 作为值返回给合并后的对象. 说的有点复杂, 看个例子.
jQuery的做法主要是这样, 先修正参数,根据第一个参数是boolean还是object确定target是哪个对象. 如果有且只有一个object类型的参数, 则把这个参数里的属性copy到jQuery或者jQuery.prototype上, 扩展jQuery对象.用这个办法可以很方便的编写插件. 否则依次遍历第一个object参数之后的所有object类型对象的属性. 根据是否深度继承用不同的方式进行copy操作. extend会改变第一个参数的引用. 如果不想改变它,可以稍微变通一下,把所有的参数属性都拷贝到一个空对象上.如$.extend({}, obj1, obj2);
[/size][/size] |
|
来自: quasiceo > 《javascript》