jQuery技術解密三

2.3 破解 jQuery 選擇器接口

jQuery 選擇器功能強大,但是用法簡單,它僅僅提供了一個接口:jQuery(),也可以簡寫爲 $() 。用法如此簡單,但又具有如此強大的處理能力,使 jQuery 必然成爲衆人追捧的對象。

在上一節中,我們重點分析了 jQuery 框架的雛形,而對於選擇器並沒有深入分析,僅僅提供了一個簡單的 DOM 元素選擇作爲演示,目的是方便讀者理解該框架的架設思路和過程。本節將重點研究 jQuery 選擇器的設計思路、實現過程和工作原理。

2.3.1 簡單但很複雜的黑洞

前面說到,jQuery 提供了惟一的接口 (jQuery() 或者 $()) 使選擇器與外界進行交流。那麼這個對象是如何生成的呢?

jQuery 框架的基礎是查詢,即查詢文檔元素對象,因此我們可以認爲 jQuery 對象就是一個選擇器,並在此基礎上構建和運行查詢過濾器。

jQuery 查詢的結果是獲取 DOM 元素,這些查詢到的 DOM 元素又是如何存儲的呢?

根據前面的介紹,我們初步瞭解到它把查詢的結果存儲到 jQuery 對象內。由於查詢的結果可能是單個元素,也可能是集合,因此,jQuery 對象內應該定義了一個集合。這個集合專門負責存放查詢到的 DOM 元素。這正如 JavaScript 中的 Function 對象一樣,其內部也構建了一個集合對象 Arguments ,專門負責存儲函數的參數。

但是,Functiono 對象和 Arguments 是兩個相互獨立的概念,僅通過 arguments 屬性聯繫在一起。也就是說 Arguments 對象並非是 Function 對象的子對象,或者是它的內部組成部分。而 jQuery 對象與查詢結果的數據集合就不同了,它是完全作爲 jQuery 對象的一部分而存在的。

另外,jQuery 雖然僅提供了一個入口,但是它的構建並不只侷限於從 DOM 文檔樹中查詢到 DOM 元素,DOM 元素也有可能從別的集合中轉移過來的,或者是從 HTML 片斷生成的等。

例如,類似下面的代碼在 jQuery 應用中經常會看到。

$("div.red").css("display", "none");       // 將 class 爲 red 的 div 元素隱藏顯示

var width = $("div .red").width();            // 獲取 div 元素下 class 爲 red 的元素的寬度

var html = $(document.getElementById("wrap")).html();       // 獲取 id 爲 wrap 元素的 innerHTML 值

$("#wrap", document.forms[0]).css("color", "red"); // 將在第一個 form 元素下 id 爲 wrap 元素的字體顏色設置爲紅色

$("<div>hello,world</div>").appendTo("#wrap");   // 將 HTML 字符串信息追加到 id 爲 wrap 元素的末尾

在 $() 函數中可以包含選擇字符串、HTML 字符串、 DOM 對象和數據集合等不同類型的參數。jQuery 是如何分辨這些參數是選擇符字符串、HTML字符串、DOM對象或數據集合的呢?

爲了方便讀者理解這其中的奧妙,我們不妨把 jQuery 框架進行簡化,先刪除所有方法、函數以及邏輯代碼,然後在 init() 構造器中,使用 alert() 方法獲取 selector 參數的類型和信息,其代碼如下。

[html] view plaincopy


  1. <script type="text/javascript">  
  2. (function(){  
  3.     var window = this;  
  4.     jQuery = window.jQuery = window.$ = function(selector, context){  
  5.         return new jQuery.fn.init(selector, context);  
  6.     };  
  7.     jQuery.fn = jQuery.prototype = {  
  8.         init: function(selector, context){  
  9.             alert(selector);  
  10.         }  
  11.     };  
  12. })();  
  13. window.onload = function(){  
  14.     $("div.red");       // 獲取 "div.red"  
  15.     $("div .red");      // 獲取 "div .red"  
  16.     $(document.getElementById("wrap")); // 獲取 "[object]"  
  17.     $("#wrap", document.forms[0]);  // 獲取 "#wrap"  
  18.     $("<div>hello,world</div>");    // 獲取 "<div>hello,world</div>"  
  19. };  
  20. </script>  
  21. <div id="wrap"></div>  

2.3.2 盤根錯節的邏輯關係

根據 jQuery 官網提供的 API 文檔可知, jQuery() 提供了以下 4 種構建 jQuery 對象的方式。

  • jQuery(expression, [context])
  • jQuery(html, [ownerDocument])
  • jQuery(elements)
  • jQuery(callback)
其中 jQuery 可以使用 $ 簡寫。上述四種構建 jQuery 對象的方式是經常用到的。從上述參數列表可以看出,其實 jQuery 的參數可以是任意元素。例如:
$("div > p"); // 參數可以是字符串
( ("div >p") ); // 參數可以是 jQuery 對象或者類數組 (ArrayLike) 的集合
$(document); // 參數可以是 DOM 元素
();<span style="white-space:pre"> </span>//&nbsp;<strong><span style="color:rgb(255,0,0)"> (document) 簡寫
(function(){});<span style="white-space:pre"> </span>//&nbsp;<strong><span style="color:rgb(255,0,0)"> (document).ready() 的簡寫
$([]); // 參數可以是數組
$({}); // 參數可以是對象
$(1); // 參數可以是數字,即把 1 存儲在 jQuery 對象的數據集合中
雖然說,在上面的示例中最後 4 行代碼都可以被解析,但是這些參數數據是被存儲到 ArrayLike (類數組) 集合中的,而不是被轉換爲 DOM 元素。雖然語法不錯,解析正常,但是它們無法完成實際應用,所以不建議傳入非 DOM 元素的參數。
注意:jQuery 對象的方法都是針對 DOM 元素對象進行的操作,如果不清楚其使用的話,很有可能會導致錯誤。
下面我們就順着 jQuery 框架的這個惟一入口,慢慢向裏爬進,以窺視其中的祕密。
***** 當我們調用 jQuery() 方法時,它沒有被實例化,也就是說 jQuery 類型被拋棄了,我們僅僅把它作爲一個普通函數來調用,此時該方法中的 this 關鍵字指向的是 Window 對象,而不是 jQuery 對象,請讀者務必注意。 ******
不過,當調用該方法時,會返回一個 jQuery.fn.init 類型的實例,同時,jQuery 又使用自己的原型對象覆蓋了 jQuery.fn.init 類型的原型對象,所以就形成了一種錯覺,很多初學者往往在這裏栽了跟頭。下面是 jQuery 框架中的核心代碼 (節選) 。
jQuery = window.jQuery = window.$ = function(selector, context){
return new jQuery.fn.init(selector, context);
};
jQuery.fn.init.prototype = jQuery.fn;
jQuery 對象不是通過 new jQuery 來繼承其 prototype 中的方法的,而是通過 jQuery.fn.init 初始化構造器生成的。所以,爲 jQuery.prototype 添加函數集也就失去了存在價值。雖然直接使用 new jQuery() 也是允許的,但是由於該函數的返回值覆蓋了 new jQuery() 創建的實例對象,所以使用 new jQuery() 來構建 jQuery 對象也是無法存活的。 (—???—-)
=====  總之,jQuery 對象其實就是 jQuery.fn.init 構造器創建的對象,而通過 jQuery.fn.init.prototype = jQuery.fn; 途徑,再使用 jQuery 的原型對象去覆蓋 jQuery.fn 的原型對象,使得 jQuery 對象的原型方法也就被繼承過來,從而形成了錯綜複雜但又井然有序的關係。 =====

2.3.3 jQuery 構造器

jQuery.fn.init() 負責對傳入參數進行分析,然後生成並返回 jQuery 對象。jQuery.fn.init() 構造器的第一個參數是必須的,如果爲空,則默認爲 document 。
從本質上講,使用 jQuery 選擇器 (即 jQuery.fn.init() 構造器) 構建jQuery對象,就是在this 對象上附加 DOM 元素集合。附加的方式包括以下兩類。
  • 如果是單個 DOM 元素,可以直接把 DOM 元素作爲數組元素傳遞給 this 對象,還可以通過 ID 從 DOM 文檔中查詢元素。
  • 如果是多個 DOM 元素,則以集合形式附加,如 jQuery 對象、數組和對象等,此時可以通過 CSS 選擇器匹配到所有 DOM 元素,然後過濾,最後構建類數組的數據結構。
而 CSS 選擇器,則是通過 jQuery().find(selector) 函數來完成的。通過 jQuery().find(selector) 可以分析選擇器字符串,並在 DOM 文檔樹中查找到符合語法的元素集合。這個函數我們將在下面章節進行分析。該函數能夠兼容 CSS1 ~ CSS3 選擇器。
下面就從 init() 初始化構造器函數開始,來分析 jQuery 選擇器是如何工作的。爲了方便解釋,我們先結合源代碼進行講解。
[html] view plaincopy


  1. <script type="text/javascript">  
  2. (function(){  
  3. var   
  4.     window = this,  
  5.     jQuery = window.jQuery = window.$ = function(selector, context){  
  6.         return new jQuery.fn.init(selector, context);  
  7.     },  
  8.     quickExpr = /^[^<]*(<(.|\s)+>)[^>]*|^#([\w-]&#43;) /;  
  9.       
  10. // jQuery 原型對象  
  11. // 構造 jQuery 對象的入口  
  12. // 所有 jQuery 對象方法都通過 jQuery 原型對象來繼承  
  13. jQuery.fn = jQuery.prototype = {  
  14.     // jQuery 對象初始化構造器,相當於 jQuery 對象的類型,由該函數負責創建 jQuery 對象  
  15.     // 參數說明:selector: 選擇器的符號,可以是任意數據類型。考慮DOM元素操作需要,該參數應該是包含DOM元素的任何數據  
  16.     // context: 上下文,指定在文檔DOM中哪個節點下開始進行查詢,默認值爲 document  
  17.     init: function(selector, context){  
  18.         selector = selector || document; // 確保 selector 參數存在,默認值爲document  
  19.         // 第一種情況,處理選擇符爲 DOM 元素,此時將忽略上下文,即忽略第二個參數  
  20.         // 例如,$(document.getElementById("wrap")), jQuery(DOMElement) 匹配DOM元素。  
  21.         // 先使用 selector.nodeType 判斷當 selector 爲元素節點,將 length 設置爲 1,  
  22.         // 並且賦值給 context ,實際上 context 作爲 init 的第二個參數,  
  23.         // 也意味着它的上下文節點就是 selector 該點,返回它的 $(DOMElement) 對象  
  24.         if(selector.nodeType){  // 存在 nodeType 屬性,說明選擇符是一個 DOM 元素  
  25.             this[0] = selector; // 直接把當前參數的 DOM 元素存入類數組中  
  26.             this.length = 1;    // 設置類數組的長度,以方便遍歷訪問  
  27.             this.context = selector; // 設置上下文屬性  
  28.             return this;  // 返回 jQuery 對象,即類數組對象  
  29.         }  
  30.         // 如果選擇符參數爲字符串,則進行處理  
  31.         // 例如,$("<div>hello,world</div>"), jQuery(html, [ownerDocument]) 匹配HTML字符串  
  32.         if(typeof selector == "string"){  
  33.             // 使用 quickExpr 正則表達式匹配該選擇符字符串,決定是處理 HTML 字符串,還是處理 ID 字符串  
  34.             // quickExpr = /^[^<]*(<(.|\s)+>)[^>]*|^#([\w-]&#43;) /   
  35.             // quickExpr 匹配 包含 < > 的字符串 或 # 後跟 [a-zA-Z0-9_] 或 - 的字符串  
  36.             var match = quickExpr.exec(selector);  
  37.             // 驗證匹配的信息,任何情況下都不是 #id  
  38.             if (match && (match[1] || !context)){  
  39.                 // 第二種情況,處理 HTML 字符串,類似 (html)&nbsp;-<span class="tag" style="margin:0px; padding:0px; border:none; color:rgb(153,51,0); background-color:inherit; font-weight:bold">&gt;</span><span style="margin:0px; padding:0px; border:none; background-color:inherit">&nbsp; (array)  
  40.                 if (match[1]){  
  41.                     //selector = jQuery.clean( [ match[1] ], context );  
  42.                 }  
  43.                 // 第三種情況,處理 ID 字符串,類似 $("#id")  
  44.                 else {  
  45.                     var elem = document.getElementById(match[3]); // 獲取該元素確保元素存在  
  46.                     // 處理在 IE 和 Opera 瀏覽器下根據 name,而不是 ID 返回元素  
  47.                     if(elem && elem.id != match[3]){  
  48.                         //return jQuery().find( selector ); // 默認調用 document.find() 方法  
  49.                     }  
  50.                     // 否則將把 elem 作爲元素參數直接調用 jQuery() 函數,返回 jQuery 對象  
  51.                     var ret = jQuery( elem || [] );  
  52.                     ret.context = document;  // 設置 jQuery 對象的上下文屬性  
  53.                     ret.selector = selector; // 設置 jQuery 對象的選擇符屬性  
  54.                     return ret;  
  55.                 }     
  56.             }else{  
  57.                 // 第四種情況,處理 jQuery(expression, [context])  
  58.                 // 例如,$("div .red") 的表達式字符串  
  59.                 //return jQuery(context).find(selector);  
  60.             }  
  61.         } //else if ( jQuery.isFunction( selector ) )  
  62.             // 第五種情況,處理 jQuery(callback),即 $(document).ready() 的簡寫  
  63.             // 例如,$(function(){alert("hello,world");}),  
  64.             // 或者 $(document).ready(function(){alert("hello,world");});  
  65.             // return jQuery( document ).ready( selector );  
  66.         // 確保舊的選擇符能夠通過  
  67.         if (selector.selector && selector.context){  
  68.             this.selector = selector.selector;  
  69.             this.context = selector.context;  
  70.         }  
  71.         // 第六種情況,處理類似 $(elements)  
  72.         //return this.setArray(jQuery.isArray(selector)? selector: jQuery.makeArray(selector));  
  73.     }  
  74. };  
  75.   
  76. })();  
  77. </script>  


進一步分析 init() 構造器函數的設計思路如下。
(1) 第一步,當第一個參數爲 DOM 元素,則廢棄第二個參數,直接把 DOM 元素存儲到 jQuery 對象的集合中,返回該 jQuery 對象。
(2) 第二步,如果第一個參數是字符串,則可能存在三種情況。
  • 情況一,第一個參數是 HTML 標籤字符串,第二個參數可選,則執行 selector = jQuery.clean([match[1]], context); 該語句能夠把 HTML 字符串轉換成 DOM 對象的數組,然後執行 Array 類型數組並返回 jQuery 對象。
  • 情況二,第一個參數是 #id 字符串,即類似 $(id),則先使用 document.getElementById() 方法獲取該元素,如果沒有獲得元素,則設置 selector = [],轉到執行 Array 類型,並返回空集合的 jQuery 對象。如果獲得元素,則構建 jQuery 對象並返回。這裏把 #id 單獨列出,是爲了提高性能。
  • 情況三,處理複雜的 CSS 選擇符字符串,第二個參數是可選的。通過 return jQuery().find(selector); 語句實現。該語句先執行 jQuery(context) ,可以看出第二個參數 context 可以是任意值,也可以是集合數據。然後調用 find(selector) 找到 jQuery(context) 上下文中所有的 DOM 元素,即這些元素都滿足 selector 表達式,最後構建 jQuery 對象並返回。
(3) 第三步,如果第一個參數是函數,則第二個參數可選。它是 $(document).ready(fn) 形式的簡寫,return jQuery(document)[jQuery.fn.ready? "ready": "load"](selector) 是其執行的代碼。該語句先執行 jQuery(document) ,再通過 new jQuery.fn.init() 方式創建 jQuery 對象,此時元素爲 document 。再調用這個對象的 ready() 方法,並返回當前的 jQuery 對象。
(document).ready(fn)domReadyjQuery (fn) 註冊 domReady 的監聽函數。所有的調用 jQuery 實現功能的代碼都應該在 domReady 之後才能夠運行。(fn) (fn) 註冊。
(4) 第四步,如果第一個參數是除 DOM 元素、函數和字符串之外的所有其他類型,也可以爲空 (如())調returnthis.setArray(jQuery.makeArray(selector));jQuerygetElementsByTagDOM (this) 。selector 還可能是單個任意對象,轉換成標準的數組之後,執行 this.setArray 把這個數組中的元素全部存儲到當前的 jQuery對象集合中,並返回 jQuery 對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章