首页 > 代码库 > jQuery内核详解与实践读书笔记1:原型技术分解1

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属性值为024          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属性值为024          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属性值为024                 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 =http://www.mamicode.com/== undefined ? "color: #008080;">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=http://www.mamicode.com/object[0]; ifalse; value=http://www.mamicode.com/object[i++]) {}73             }74         }75         return object;//返回jQuery对象76     }77 });
View Code

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

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

 

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

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