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 參數的類型和信息,其代碼如下。
- <script type="text/javascript">
- (function(){
- var window = this;
- jQuery = window.jQuery = window.$ = function(selector, context){
- return new jQuery.fn.init(selector, context);
- };
- jQuery.fn = jQuery.prototype = {
- init: function(selector, context){
- alert(selector);
- }
- };
- })();
- window.onload = function(){
- $("div.red"); // 獲取 "div.red"
- $("div .red"); // 獲取 "div .red"
- $(document.getElementById("wrap")); // 獲取 "[object]"
- $("#wrap", document.forms[0]); // 獲取 "#wrap"
- $("<div>hello,world</div>"); // 獲取 "<div>hello,world</div>"
- };
- </script>
- <div id="wrap"></div>
2.3.2 盤根錯節的邏輯關係
根據 jQuery 官網提供的 API 文檔可知, jQuery() 提供了以下 4 種構建 jQuery 對象的方式。
- jQuery(expression, [context])
- jQuery(html, [ownerDocument])
- jQuery(elements)
- jQuery(callback)
2.3.3 jQuery 構造器
- 如果是單個 DOM 元素,可以直接把 DOM 元素作爲數組元素傳遞給 this 對象,還可以通過 ID 從 DOM 文檔中查詢元素。
- 如果是多個 DOM 元素,則以集合形式附加,如 jQuery 對象、數組和對象等,此時可以通過 CSS 選擇器匹配到所有 DOM 元素,然後過濾,最後構建類數組的數據結構。
- <script type="text/javascript">
- (function(){
- var
- window = this,
- jQuery = window.jQuery = window.$ = function(selector, context){
- return new jQuery.fn.init(selector, context);
- },
- quickExpr = /^[^<]*(<(.|\s)+>)[^>]*|^#([\w-]+) /;
- // jQuery 原型對象
- // 構造 jQuery 對象的入口
- // 所有 jQuery 對象方法都通過 jQuery 原型對象來繼承
- jQuery.fn = jQuery.prototype = {
- // jQuery 對象初始化構造器,相當於 jQuery 對象的類型,由該函數負責創建 jQuery 對象
- // 參數說明:selector: 選擇器的符號,可以是任意數據類型。考慮DOM元素操作需要,該參數應該是包含DOM元素的任何數據
- // context: 上下文,指定在文檔DOM中哪個節點下開始進行查詢,默認值爲 document
- init: function(selector, context){
- selector = selector || document; // 確保 selector 參數存在,默認值爲document
- // 第一種情況,處理選擇符爲 DOM 元素,此時將忽略上下文,即忽略第二個參數
- // 例如,$(document.getElementById("wrap")), jQuery(DOMElement) 匹配DOM元素。
- // 先使用 selector.nodeType 判斷當 selector 爲元素節點,將 length 設置爲 1,
- // 並且賦值給 context ,實際上 context 作爲 init 的第二個參數,
- // 也意味着它的上下文節點就是 selector 該點,返回它的 $(DOMElement) 對象
- if(selector.nodeType){ // 存在 nodeType 屬性,說明選擇符是一個 DOM 元素
- this[0] = selector; // 直接把當前參數的 DOM 元素存入類數組中
- this.length = 1; // 設置類數組的長度,以方便遍歷訪問
- this.context = selector; // 設置上下文屬性
- return this; // 返回 jQuery 對象,即類數組對象
- }
- // 如果選擇符參數爲字符串,則進行處理
- // 例如,$("<div>hello,world</div>"), jQuery(html, [ownerDocument]) 匹配HTML字符串
- if(typeof selector == "string"){
- // 使用 quickExpr 正則表達式匹配該選擇符字符串,決定是處理 HTML 字符串,還是處理 ID 字符串
- // quickExpr = /^[^<]*(<(.|\s)+>)[^>]*|^#([\w-]+) /
- // quickExpr 匹配 包含 < > 的字符串 或 # 後跟 [a-zA-Z0-9_] 或 - 的字符串
- var match = quickExpr.exec(selector);
- // 驗證匹配的信息,任何情況下都不是 #id
- if (match && (match[1] || !context)){
- // 第二種情況,處理 HTML 字符串,類似 (html) -<span class="tag" style="margin:0px; padding:0px; border:none; color:rgb(153,51,0); background-color:inherit; font-weight:bold">></span><span style="margin:0px; padding:0px; border:none; background-color:inherit"> (array)
- if (match[1]){
- //selector = jQuery.clean( [ match[1] ], context );
- }
- // 第三種情況,處理 ID 字符串,類似 $("#id")
- else {
- var elem = document.getElementById(match[3]); // 獲取該元素確保元素存在
- // 處理在 IE 和 Opera 瀏覽器下根據 name,而不是 ID 返回元素
- if(elem && elem.id != match[3]){
- //return jQuery().find( selector ); // 默認調用 document.find() 方法
- }
- // 否則將把 elem 作爲元素參數直接調用 jQuery() 函數,返回 jQuery 對象
- var ret = jQuery( elem || [] );
- ret.context = document; // 設置 jQuery 對象的上下文屬性
- ret.selector = selector; // 設置 jQuery 對象的選擇符屬性
- return ret;
- }
- }else{
- // 第四種情況,處理 jQuery(expression, [context])
- // 例如,$("div .red") 的表達式字符串
- //return jQuery(context).find(selector);
- }
- } //else if ( jQuery.isFunction( selector ) )
- // 第五種情況,處理 jQuery(callback),即 $(document).ready() 的簡寫
- // 例如,$(function(){alert("hello,world");}),
- // 或者 $(document).ready(function(){alert("hello,world");});
- // return jQuery( document ).ready( selector );
- // 確保舊的選擇符能夠通過
- if (selector.selector && selector.context){
- this.selector = selector.selector;
- this.context = selector.context;
- }
- // 第六種情況,處理類似 $(elements)
- //return this.setArray(jQuery.isArray(selector)? selector: jQuery.makeArray(selector));
- }
- };
- })();
- </script>
進一步分析 init() 構造器函數的設計思路如下。
- 情況一,第一個參數是 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 對象並返回。