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,如有兴趣敬请关注,主要一个码农的所看所思所想所叹,或扫描下方二维码关注:

你可能感兴趣的:(jquery)