jQuery内核详解与实践读书笔记1:原型技术分解1

  一直以来都有研究一下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 };
View Code

此时这个框架最基本的样子就孕育出来了。这几行代码都很简单,但却是整个框架的基础。

 

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 }; 
View Code

 

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 }; 
View Code

上述代码中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 };
View Code

这样就可以把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 }; 
View Code

但是,这种方式也会带来另一个问题:无法访问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的原型对象
View Code

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的原型对象 
View Code

这里就实现了一个最简单的选择器了,当然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 };
View Code

注意:在上面的代码中,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 });
View Code

同时jQuery框架定义的html()方法包含的功能比较多,它不仅可以插入HTML源代码,还可以返回匹配元素包含的HTML源代码,故使用了一个条件结构分别进行处理。首先,判断参数是否为空,如果为空,则表示获取匹配元素中第一个元素包含的HTML源代码,此时返回该innerHTML的值。如果不为空,则先清空匹配元素中每个元素包含的内容,并使用append()方法插入HTML源代码。

好了,暂时只看到了这里,下次把剩下的三步完成。

 

个人微信公众号:programmlife,如有兴趣敬请关注,主要一个码农的所看所思所想所叹,或扫描下方二维码关注:

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。