一直以來都有研究一下jQuery源代碼的想法,可是每次看到jQuery幾千行的代碼,頭就大了,沒有一點頭緒,也不知道從哪裏開始。昨天去圖書館無心間發現了這本《jQuery內核詳解和實踐》,翻看了一下里面的內容,這正是我尋覓多時剖析jQuery源碼的好書。html
廢話很少說,直入正題吧。第一章介紹了一下jQuery的起步和一些歷史故事,沒什麼重要內容。這裏直接進入第二章,jQuery技術解密,從這一章開始就所有是乾貨了。這一章主要分四部分:jQuery原型技術分解,破解jQuery選擇器接口,解析jQuery選擇器引擎Sizzle,類數組。node
jQuery原型技術分解主要就是從0開始一步步搭建一個簡易的jQuery框架,講述了jQuery框架的搭建過程,書中主要分紅了9個步驟,最後造成一個jQuery框架的雛形。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. 生命--返回實例app
若是用上面的代碼時,獲得一個jQuery的對象是須要new出來的,可是咱們使用的jQuery並非經過new來獲得jQuery對象的,而是經過$()獲得的。jQuery是如何實現$()的方式進行函數的調用?框架
咱們應該把jQuery看作是一個類,同時也應該把它視爲一個普通的函數,並讓這個函數的返回值爲jQuery類的實例。可是若是直接在jQuery函數中返回一個new出來的jQuery實例,會形成死循環,致使內存外溢。ide
考慮:在建立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,若有興趣敬請關注,主要一個碼農的所看所思所想所嘆,或掃描下方二維碼關注: