第二章 jQuery技術解密 (三)

mybwu_com發表於2013-12-28

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 引數的型別和資訊,其程式碼如下。

  1. <scripttype="text/javascript">
  2. (function(){
  3. varwindow=this;
  4. jQuery=window.jQuery=window.$=function(selector,context){
  5. returnnewjQuery.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. <divid="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 元素
$(); //$(document) 簡寫
$(function(){}); //$(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 選擇器是如何工作的。為了方便解釋,我們先結合原始碼進行講解。
  1. <scripttype="text/javascript">
  2. (function(){
  3. var
  4. window=this,
  5. jQuery=window.jQuery=window.$=function(selector,context){
  6. returnnewjQuery.fn.init(selector,context);
  7. },
  8. quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/;
  9. //jQuery原型物件
  10. //構造jQuery物件的入口
  11. //所有jQuery物件方法都通過jQuery原型物件來繼承
  12. jQuery.fn=jQuery.prototype={
  13. //jQuery物件初始化構造器,相當於jQuery物件的型別,由該函式負責建立jQuery物件
  14. //引數說明:selector:選擇器的符號,可以是任意資料型別。考慮DOM元素操作需要,該引數應該是包含DOM元素的任何資料
  15. //context:上下文,指定在文件DOM中哪個節點下開始進行查詢,預設值為document
  16. init:function(selector,context){
  17. selector=selector||document;//確保selector引數存在,預設值為document
  18. //第一種情況,處理選擇符為DOM元素,此時將忽略上下文,即忽略第二個引數
  19. //例如,$(document.getElementById("wrap")),jQuery(DOMElement)匹配DOM元素。
  20. //先使用selector.nodeType判斷當selector為元素節點,將length設定為1,
  21. //並且賦值給context,實際上context作為init的第二個引數,
  22. //也意味著它的上下文節點就是selector該點,返回它的$(DOMElement)物件
  23. if(selector.nodeType){//存在nodeType屬性,說明選擇符是一個DOM元素
  24. this[0]=selector;//直接把當前引數的DOM元素存入類陣列中
  25. this.length=1;//設定類陣列的長度,以方便遍歷訪問
  26. this.context=selector;//設定上下文屬性
  27. returnthis;//返回jQuery物件,即類陣列物件
  28. }
  29. //如果選擇符引數為字串,則進行處理
  30. //例如,$("<div>hello,world</div>"),jQuery(html,[ownerDocument])匹配HTML字串
  31. if(typeofselector=="string"){
  32. //使用quickExpr正規表示式匹配該選擇符字串,決定是處理HTML字串,還是處理ID字串
  33. //quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#([\w-]+)$/
  34. //quickExpr匹配包含<>的字串或#後跟[a-zA-Z0-9_]或-的字串
  35. varmatch=quickExpr.exec(selector);
  36. //驗證匹配的資訊,任何情況下都不是#id
  37. if(match&&(match[1]||!context)){
  38. //第二種情況,處理HTML字串,類似$(html)->$(array)
  39. if(match[1]){
  40. //selector=jQuery.clean([match[1]],context);
  41. }
  42. //第三種情況,處理ID字串,類似$("#id")
  43. else{
  44. varelem=document.getElementById(match[3]);//獲取該元素確保元素存在
  45. //處理在IE和Opera瀏覽器下根據name,而不是ID返回元素
  46. if(elem&&elem.id!=match[3]){
  47. //returnjQuery().find(selector);//預設呼叫document.find()方法
  48. }
  49. //否則將把elem作為元素引數直接呼叫jQuery()函式,返回jQuery物件
  50. varret=jQuery(elem||[]);
  51. ret.context=document;//設定jQuery物件的上下文屬性
  52. ret.selector=selector;//設定jQuery物件的選擇符屬性
  53. returnret;
  54. }
  55. }else{
  56. //第四種情況,處理jQuery(expression,[context])
  57. //例如,$("div.red")的表示式字串
  58. //returnjQuery(context).find(selector);
  59. }
  60. }//elseif(jQuery.isFunction(selector))
  61. //第五種情況,處理jQuery(callback),即$(document).ready()的簡寫
  62. //例如,$(function(){alert("hello,world");}),
  63. //或者$(document).ready(function(){alert("hello,world");});
  64. //returnjQuery(document).ready(selector);
  65. //確保舊的選擇符能夠通過
  66. if(selector.selector&&selector.context){
  67. this.selector=selector.selector;
  68. this.context=selector.context;
  69. }
  70. //第六種情況,處理類似$(elements)
  71. //returnthis.setArray(jQuery.isArray(selector)?selector:jQuery.makeArray(selector));
  72. }
  73. };
  74. })();
  75. </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) 是實現 domReady 的 jQuery 物件的統一入口,可以通過 $(fn) 註冊 domReady 的監聽函式。所有的呼叫 jQuery 實現功能的程式碼都應該在 domReady 之後才能夠執行。$(fn) 是所有應用開發中的功能程式碼的入口,它支援任意多的 $(fn) 註冊。
(4) 第四步,如果第一個引數是除 DOM 元素、函式和字串之外的所有其他型別,也可以為空 (如$()),而第二個引數可選。呼叫 return this.setArray(jQuery.makeArray(selector)); 進行處理時,它先是把第一個引數轉換為陣列。當然這個引數可以是類陣列結構的集合,如 jQuery 物件、getElementsByTag 返回的 DOM 元素集合等,可支援 $(this) 。selector 還可能是單個任意物件,轉換成標準的陣列之後,執行 this.setArray 把這個陣列中的元素全部儲存到當前的 jQuery物件集合中,並返回 jQuery 物件。

相關文章