一直以来都有研究一下jQuery源代码的想法,但是每次看到jQuery几千行的代码,头就大了,没有一点头绪,也不知道从哪里开始。昨天去图书馆无意间发现了这本《jQuery内核详解和实践》,翻看了一下里面的内容,这正是我寻觅多时剖析jQuery源码的好书。
废话不多说,直入正题吧。第一章介绍了一下jQuery的起步和一些历史故事,没什么重要内容。这里直接进入第二章,jQuery技术解密,从这一章开始就全部是干货了。这一章主要分四部分:jQuery原型技术分解,破解jQuery选择器接口,解析jQuery选择器引擎Sizzle,类数组。
jQuery原型技术分解主要就是从0开始一步步搭建一个简易的jQuery框架,讲述了jQuery框架的搭建过程,书中主要分成了9个步骤,最后形成一个jQuery框架的雏形。
1. 起源--原型继承
模仿jQuery框架源码,添加两个成员,一个原型属性jquery,一个原型方法size(),源代码如下:
1 var $ = jQuery = function() {}; 2 jQuery.fn = jQuery.prototype = { 3 jquery : "1.3.2", //原型属性 4 size : function() { //原型方法 5 return this.length; 6 } 7 };
此时这个框架最基本的样子就孕育出来了。这几行代码都很简单,但却是整个框架的基础。
2. 生命--返回实例
如果用上面的代码时,得到一个jQuery的对象是需要new出来的,但是我们使用的jQuery并不是通过new来得到jQuery对象的,而是通过$()得到的。jQuery是如何实现$()的方式进行函数的调用?
我们应该把jQuery看做是一个类,同时也应该把它视为一个普通的函数,并让这个函数的返回值为jQuery类的实例。但是如果直接在jQuery函数中返回一个new出来的jQuery实例,会造成死循环,导致内存外溢。
考虑:在创建jQuery类实例时,this关键字就是指向对象实例的,而且不论是在jQuery.prototype中原型属性还是方法,this关键字总是指向类的实例。
结论:在jQuery中使用一个工厂方法来创建一个实例,把这个方法放在jQuery.prototype 原型对象中,然后在jQuery()函数中返回这个原型方法的调用。
这样就可以将1中的代码修改成下面的代码:
1 var $ = jQuery = function() { 2 return jQuery.fn.init(); //调用原型方法init() 3 }; 4 jQuery.fn = jQuery.prototype = { 5 init : function() { //在初始化原型方法中返回实例的引用 6 return this; 7 }, 8 jquery : "1.3.2", //原型属性 9 size : function() { //原型方法 10 return this.length; 11 } 12 };
3. 学步--分隔作用域
如果按照2的代码,我们又会出现问题。如下代码:
1 var $ = jQuery = function() { 2 return jQuery.fn.init(); //调用原型方法init() 3 }; 4 jQuery.fn = jQuery.prototype = { 5 init : function() { //在初始化原型方法中返回实例的引用 6 this.length = 0; 7 this.test = function() { 8 return this.length; 9 }; 10 return this; 11 }, 12 jquery : "1.3.2", //原型属性 13 length : 1, 14 size : function() { //原型方法 15 return this.length; 16 } 17 };
上述代码中jQuery原型对象中包含一个length属性,同时init()从一个普通函数变成了构造器,它也包含一个length属性和一个test()方法。this关键字引用了init()函数作用域所在的对象,此时它访问length属性时,返回0.而this关键字也能够访问上一级对象jQuery.fn对象的作用域,所以$().jquery返回"1.3.2"。但是调用$().size()方法时,返回的是0,而不是1?
解决方法:jQuery框架是通过下面的方式调用init()初始化构造函数,达到隔离作用域的目的:
1 var $ = jQuery = function() { 2 return new jQuery.fn.init(); //实例化init初始化类型,分隔作用域 3 };
这样就可以把init()构造器中的this和jQuery.fn对象中的this关键字隔离开来,避免相互混淆。
此时源代码就变成如下:
1 var $ = jQuery = function() { 2 return new jQuery.fn.init(); //实例化init初始化类型,分隔作用域 3 }; 4 jQuery.fn = jQuery.prototype = { 5 init : function() { //在初始化原型方法中返回实例的引用 6 this.length = 0; 7 this.test = function() { 8 return this.length; 9 }; 10 return this; 11 }, 12 jquery : "1.3.2", //原型属性 13 length : 1, 14 size : function() { //原型方法 15 return this.length; 16 } 17 };
但是,这种方式也会带来另一个问题:无法访问jQuery.fn对象的属性或方法。
4. 生长--跨域访问
上一节抛出了一个问题:无法访问jQuery.fn对象的属性或方法,如何解决?
方法:通过原型传递,jQuery框架把jQuery.fn传递给jQuery.fn.init.prototype,也就是说用jQuery的原型对象覆盖init构造器的原型对象,从而实现跨域访问,其源代码如下:
1 var $ = jQuery = function() { 2 return new jQuery.fn.init(); //实例化init初始化类型,分隔作用域 3 }; 4 jQuery.fn = jQuery.prototype = { 5 init : function() { //在初始化原型方法中返回实例的引用 6 this.length = 0; 7 this.test = function() { 8 return this.length; 9 }; 10 return this; 11 }, 12 jquery : "1.3.2", //原型属性 13 length : 1, 14 size : function() { //原型方法 15 return this.length; 16 } 17 }; 18 jQuery.fn.init.prototype = jQuery.fn; //使用jQuery的原型对象覆盖init的原型对象
new jQuery.fn.init()创建的新对象拥有init构造器的prototype原型对象的方法,通过改变prototype指针的指向,使其指向jQuery类的prototype,这样创建出来的对象就继承了jQuery.fn原型对象定义的方法。
5. 成熟--选择器
jQuery函数包含两个参数selector和context,其中selector表示选择器,而context表示的内容范围,它表示一个DOM元素。在此,为了简化操作,假设选择器的类型仅限定为标签选择器,其实现代码如下:
1 var $ = jQuery = function(selector, context) { //定义类 2 return new jQuery.fn.init(selector, context); //返回选择器的实例 3 }; 4 jQuery.fn = jQuery.prototype = { //jQuery类的原型对象 5 init : function(selector, context) { //定义选择器构造器 6 selector = selector || document; //设置默认值为document 7 context = context || document; //设置默认值为document 8 if(selector.nodeType) { //如果选择符为节点对象 9 this[0] = selector; //把参数节点传递给实例对象的数组 10 this.length = 1; //并设置实例对象的length属性,定义包含的元素个数 11 this.context = selector; //设置实例的属性,返回选择范围 12 return this; //返回当前实例 13 } 14 if(typeof selector === "string") { //如果选择符是字符串 15 var e = context.getElementsByTagName(selector); //获取指定名称的元素 16 for(var i=0; i<e.length; i++) { //遍历元素集合,并把所有元素填入到当前实例数组中 17 this[i] = e[i]; 18 } 19 this.length = e.length; //设置实例的length属性,即定义包含的元素个数 20 this.context = context; //设置实例的属性,返回选择范围 21 return this; //返回当前实例 22 } else { 23 this.length = 0; //否则,设置实例的length属性值为0 24 this.context = context; //设置实例的属性,返回选择范围 25 return this; //返回当前实例 26 } 27 }, 28 jquery : "1.3.2", //原型属性 29 size : function() { //原型方法 30 return this.length; 31 } 32 }; 33 jQuery.fn.init.prototype = jQuery.fn; //使用jQuery的原型对象覆盖init的原型对象
这里就实现了一个最简单的选择器了,当然jQuery框架中的选择器比这里的要复杂的多,这里只是为了搭建一个jQuery框架的最简单形式,以后再收入去研究它的选择器。
6. 延续--迭代器
在jQuery框架中,jQuery对象是一个比较特殊的对象,具有多重身份,可以分解如下:
第一, jQuery对象是一个数组集合,它不是一个个具体对象。因此,无法直接使用JavaScript的方法来操作它。
第二, jQuery对象实际上就是一个普通的对象,因为它是通过new运算符创建的一个新的实例对象。它可以继承原型方法或属性,同样也拥有Object类型的方法和属性。
第三, jQuery对象包含数组特性,因为它赋值了数组元素,以数组结构存储返回的数据。可以以JavaScript的概念理解jQuery对象,jQuery对象就是对象和数组的混合体,但是它不拥有数组的方法,因为它的数组结构是人为附加的,也就是说它不是Array类型数据,而是Object类型数据。
第四, jQuery对象包含的数据都是DOM元素,是通过数组形式存储的,即通过jQuery[n]形式获取。同时jQuery对象又定义了几个模仿Array基本特性的属性,如length等
所以,jQuery对象是不允许直接操作的,只有分别读取它包含的每一个DOM元素,才能够实现各种操作,如插入,删除,嵌套,赋值和读写DOM元素属性等。
如何实现直接操作jQuery对象中的DOM元素呢?例如$("div").html()
jQuery定义了一个工具函数each(),利用这个工具函数可以遍历jQuery对象中所有的DOM元素,并把需要操作的内存封装到一个回调函数中,然后通过在每个DOM元素上调用这个回调函数即可。实现代码如下:
1 var $ = jQuery = function(selector, context) { //定义类 2 return new jQuery.fn.init(selector, context); //返回选择器的实例 3 }; 4 jQuery.fn = jQuery.prototype = { //jQuery类的原型对象 5 init : function(selector, context) { //定义选择器构造器 6 selector = selector || document; //设置默认值为document 7 context = context || document; //设置默认值为document 8 if(selector.nodeType) { //如果选择符为节点对象 9 this[0] = selector; //把参数节点传递给实例对象的数组 10 this.length = 1; //并设置实例对象的length属性,定义包含的元素个数 11 this.context = selector; //设置实例的属性,返回选择范围 12 return this; //返回当前实例 13 } 14 if(typeof selector === "string") { //如果选择符是字符串 15 var e = context.getElementsByTagName(selector); //获取指定名称的元素 16 for(var i=0; i<e.length; i++) { //遍历元素集合,并把所有元素填入到当前实例数组中 17 this[i] = e[i]; 18 } 19 this.length = e.length; //设置实例的length属性,即定义包含的元素个数 20 this.context = context; //设置实例的属性,返回选择范围 21 return this; //返回当前实例 22 } else { 23 this.length = 0; //否则,设置实例的length属性值为0 24 this.context = context; //设置实例的属性,返回选择范围 25 return this; //返回当前实例 26 } 27 }, 28 jquery : "1.3.2", //原型属性 29 size : function() { //原型方法 30 return this.length; 31 }, 32 33 //定义jQuery对象方法 34 html : function(val) { //模仿jQuery框架中的html()方法,为匹配的每一个DOM元素插入html代码 35 jQuery.each(this, function(val) { //调用jQuery.each()工具函数,为每一个DOM元素执行回调函数 36 this.innerHTML = val; 37 }, val); 38 } 39 }; 40 jQuery.fn.init.prototype = jQuery.fn; //使用jQuery的原型对象覆盖init的原型对象 41 42 //扩展jQuery工具函数 43 jQuery.each = function(object, callback, args) { 44 for(var i=0; i<object.length; i++) { 45 callback.call(object[i], args); 46 } 47 return object; 48 };
注意:在上面的代码中,each()函数的当前作用对象是jQuery对象,故this指向当前jQuery对象,即this表示一个集合对象;而在html()方法中,由于each()函数是在指定DOM元素上执行的,所以该函数内的this指针指向的是当前DOM元素对象,即this表示一个元素。
以上定义的each()工具函数比较简单,适应能力很有限。在jQuery框架中,它封装的each()函数功能强大很多,具体代码如下:
1 var $ = jQuery = function(selector, context) { //定义类 2 return new jQuery.fn.init(selector, context); //返回选择器的实例 3 }; 4 jQuery.fn = jQuery.prototype = { //jQuery类的原型对象 5 init : function(selector, context) { //定义选择器构造器 6 selector = selector || document; //设置默认值为document 7 context = context || document; //设置默认值为document 8 if(selector.nodeType) { //如果选择符为节点对象 9 this[0] = selector; //把参数节点传递给实例对象的数组 10 this.length = 1; //并设置实例对象的length属性,定义包含的元素个数 11 this.context = selector; //设置实例的属性,返回选择范围 12 return this; //返回当前实例 13 } 14 if(typeof selector === "string") { //如果选择符是字符串 15 var e = context.getElementsByTagName(selector); //获取指定名称的元素 16 for(var i=0; i<e.length; i++) { //遍历元素集合,并把所有元素填入到当前实例数组中 17 this[i] = e[i]; 18 } 19 this.length = e.length; //设置实例的length属性,即定义包含的元素个数 20 this.context = context; //设置实例的属性,返回选择范围 21 return this; //返回当前实例 22 } else { 23 this.length = 0; //否则,设置实例的length属性值为0 24 this.context = context; //设置实例的属性,返回选择范围 25 return this; //返回当前实例 26 } 27 }, 28 jquery : "1.3.2", //原型属性 29 size : function() { //原型方法 30 return this.length; 31 }, 32 33 //定义jQuery对象方法 34 html : function(value) { 35 return value === undefined ? 36 (this[0] ? 37 this[0].innerHTML.repalce(/ jQuery\d+="(?:\d+|null)"/g, "") : 38 null) : 39 this.empty().append(value); 40 } 41 }; 42 jQuery.fn.init.prototype = jQuery.fn; //使用jQuery的原型对象覆盖init的原型对象 43 44 //扩展jQuery工具函数 45 jQuery.extend({ 46 //参数说明:object表示jQuery对象,callback表示回调函数,args回调函数的参数数组 47 each : function(object, callback, args) { 48 var name, i = 0, length = object.length; 49 if(args) {//如果存在回调函数的参数数组 50 if(length === undefined) {//如果object不是jQuery对象 51 for(name in object) {//遍历object的属性 52 if(callback.apply(object[name], args) === false) {//在对象上调用回调函数 53 break;//如果回调函数返回值为false,则跳出循环 54 } 55 } 56 } else {//如果object是jQuery对象 57 for( ; i< length; ) { //遍历jQuery对象数组 58 if(callback.apply(object[i++], args) === false) { //在对象上调用回调函数 59 break;//如果回调函数返回值为false,则跳出循环 60 } 61 } 62 } 63 } else { 64 if(length === undefined) {//如果object不是jQuery对象 65 for(name in object) {//遍历object对象 66 if(callback.call(object[name], name, object[name]) === false) {//在对象上调用回调函数 67 break;//如果回调函数返回值为false,则跳出循环 68 } 69 } 70 } else {//如果object是jQuery对象 71 //遍历jQuery对象数组,并在对象上调用回调函数 72 for(var value=object[0]; i<length && callback.call(value, i, value) !== false; value=object[i++]) {} 73 } 74 } 75 return object;//返回jQuery对象 76 } 77 });
同时jQuery框架定义的html()方法包含的功能比较多,它不仅可以插入HTML源代码,还可以返回匹配元素包含的HTML源代码,故使用了一个条件结构分别进行处理。首先,判断参数是否为空,如果为空,则表示获取匹配元素中第一个元素包含的HTML源代码,此时返回该innerHTML的值。如果不为空,则先清空匹配元素中每个元素包含的内容,并使用append()方法插入HTML源代码。
好了,暂时只看到了这里,下次把剩下的三步完成。
个人微信公众号:programmlife,如有兴趣敬请关注,主要一个码农的所看所思所想所叹,或扫描下方二维码关注: