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,如有興趣敬請關注,主要一個碼農的所看所思所想所嘆,或掃描下方二維碼關注:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM