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