1. 程式人生 > >jQuery 2.0.3 原始碼分析core

jQuery 2.0.3 原始碼分析core

     宣告:本文為原創文章,如需轉載,請註明來源並保留原文連結Aaron,謝謝!

     開啟jQuery原始碼,一眼看去到處都充斥著正則表示式,jQuery框架的基礎就是查詢了,查詢文件元素物件,所以狹隘的說呢,jQuery就是一個選擇器,並這個基礎上構建和執行查詢過濾器!

工欲善其事,必先利其器,所以先從正則入手

我們來分解一個表示式

// A simple way to check for HTML strings
// Prioritize #id over <tag> to avoid XSS via location.hash (#9521)
// Strict HTML recognition (#11290: must start with <) rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,

作者的解釋呢很簡單,一個簡單的檢測HTML字串的表示式

分解:

1. 通過選擇|分割二義,匹配^開頭或者$結尾

  • ^(?:\s*(<[\w\W]+>)[^>]*
  • #([\w-]*))$

2. ^(?:\s*(<[\w\W]+>)[^>]*

  • (?:pattern
    ) : 匹配 pattern 但不獲取匹配結果,也就是說這是一個非獲取匹配,不進行儲存供以後使用
  • \s* : 匹配任何空白字元,包括空格、製表符、換頁符等等 零次或多次 等價於{0,}
  • (pattern) : 匹配pattern 並獲取這一匹配。所獲取的匹配可以從產生的 Matches 集合得到,使用 $0$9 屬性
  • [\w\W]+ : 匹配於'[A-Za-z0-9_]'或[^A-Za-z0-9_]' 一次或多次, 等價{1,}
  • (<[wW]+>) :這個表示字串裡要包含用<>包含的字元,例如<p>,<div>等等都是符合要求的
  • [^>]* : 負值字元集合,字串尾部是除了>的任意字元或者沒有字元,零次或多次等價於{0,},

3. #([\w-]*))$

  • 匹配結尾帶上#號的任意字元,包括下劃線與-

4. 還要穿插一下exec方法

  • 如果執行exec方法的正則表示式沒有分組(沒有括號括起來的內容),那麼如果有匹配,他將返回一個只有一個元素的陣列,這個陣列唯一的元素就是該正則表示式匹配的第一個串;如果沒有匹配則返回null。
  • exec如果找到了匹配,而且包含分組的話,返回的陣列將包含多個元素,第一個元素是找到的匹配,之後的元素依次為該匹配中的第一、第二...個分組(反向引用)

所以綜合起來呢大概的意思就是:匹配HTML標記和ID表示式(<前面可以匹配任何空白字元,包括空格、製表符、換頁符等等)

簡單測試下:

var str = ' <div id=top></div>';
var match = rquickExpr.exec(str);
console.log(match)
//[" <div id=top></div>", "<div id=top></div>", undefined, index: 0, input: " <div id=top></div>"]
var str = '[?\f\n\r\t\v]<div id=top></div>'; 
var str = '#test';
var match = rquickExpr.exec(str);
console.log(match)
//["#test", undefined, "test", index: 0, input: "#test"]

jQuery選擇器介面

API

image

jQuery是總入口,選擇器支援9種方式的處理

1.$(document)   
2.$(‘<div>’) 
3.$(‘div’) 
4.$(‘#test’) 
5.$(function(){}) 
6.$("input:radio", document.forms[0]); 
7.$(‘input’, $(‘div’)) 
8.$() 
9.$("<div>", { 
         "class": "test", 
         text: "Click me!", 
         click: function(){ $(this).toggleClass("test"); } 
      }).appendTo("body"); 
10$($(‘.test’))

jQuery這個選擇器重構了幾次後,現在邏輯結構相當的清晰了,一看大概就知道

不能不得說jQuery的反模式,非職責單一深受開發者喜歡,一個介面承載的職責越多內部處理就越複雜了

jQuery查詢的的物件是dom元素,查詢到目標元素後,如何儲存?

  • 查詢的到結果儲存到jQuery物件內部,由於查詢的dom可能是單一元素,也可能是合集
  • jQuery內部應該要定義一個合集陣列,用於存在選擇後的dom元素,
  • 當然啦,根據API,jQuery構建的不僅僅只是DOM元素,還有HTML字串,Object,[] 等等…

本質上講jQuery.fn.init構建的出來的物件,就是jQuery物件

init: function( selector, context, rootjQuery ) {
            var match, elem;

            // HANDLE: $(""), $(null), $(undefined), $(false)
            if ( !selector ) {
                return this;
            }

            // Handle HTML strings
            if ( typeof selector === "string" ) {
                // HANDLE: $(DOMElement)
            } else if ( selector.nodeType ) {
                // HANDLE: $(function)
                // Shortcut for document ready
            } else if ( jQuery.isFunction( selector ) ) {
                return rootjQuery.ready( selector );
            }

            if ( selector.selector !== undefined ) {
                this.selector = selector.selector;
                this.context = selector.context;
            }

            return jQuery.makeArray( selector, this );
        },

原始碼縮排後的結構:

  • 處理"",null,undefined,false,返回this ,增加程式的健壯性
  • 處理字串
  • 處理DOMElement,返回修改過後的this,給this添加了
  • 處理$(function(){})

匹配模式一:$("#id")

1. 進入字串處理

if ( typeof selector === "string" ) {

2. 發現不是 "<"開始,">"結尾 $('<p id="test">My <em>new</em> text</p>')這種的情況

   如果selector是html標籤組成的話,直接match = [ null, selector, null ];

   而不用正則檢查

if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) {

3. 否則的話需要match = rquickExpr.exec( selector )

match = rquickExpr.exec( selector );

4. 匹配的html或確保沒有上下文指定為# id

if ( match && (match[1] || !context) ) {

5. match[1]存在,處理$(html) -> $(array),,也就是處理的是html方式

if ( match[1] ) {

6. 處理ID

elem = document.getElementById( match[2] );

// Check parentNode to catch when Blackberry 4.6 returns
// nodes that are no longer in the document #6963
if ( elem && elem.parentNode ) {
    // Inject the element directly into the jQuery object
    this.length = 1;
    this[0] = elem;
}

this.context = document;
this.selector = selector;
return this;

至此本次檢索完畢!

可以看到

this就是jQuery工廠化後返回的物件

  • this.length    
  • this[0] = elem
  • this.context = document;
  • this.selector = selector;

image

匹配模式二:<htmltag>

重複的地方跳過直接看處理介面

if ( match && (match[1] || !context) ) {
    // HANDLE: $(html) -> $(array)
    if ( match[1] ) {
        context = context instanceof jQuery ? context[0] : context;
        // scripts is true for back-compat
        jQuery.merge( this, jQuery.parseHTML(
            match[1],
            context && context.nodeType ? context.ownerDocument || context : document,
            true
        ) );
        // HANDLE: $(html, props)
        if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
            for ( match in context ) {
                // Properties of context are called as methods if possible
                if ( jQuery.isFunction( this[ match ] ) ) {
                    this[ match ]( context[ match ] );
                    // ...and otherwise set as attributes
                } else {
                    this.attr( match, context[ match ] );
                }
            }
        }
        return this;
        // HANDLE: $(#id)
    } else {

傳入上下文:

context && context.nodeType ? context.ownerDocument || context : document

ownerDocument和 documentElement的區別

  • ownerDocument是Node物件的一個屬性,返回的是某個元素的根節點文件物件:即document物件
  • documentElement是Document物件的屬性,返回的是文件根節點
  • 對於HTML文件來說,documentElement是<html>標籤對應的Element物件,ownerDocument是document物件

具體請看API手冊

jQuery.merge( first, second ) 合併兩個陣列內容到第一個陣列。

jQuery.parseHTML

使用原生的DOM元素的建立函式將字串轉換為一組DOM元素,然後,可以插入到文件中。

str = "hello, <b>my name is</b> jQuery.",
html = $.parseHTML( str ),

image

原始碼:

parseHTML: function( data, context, keepScripts ) {
    if ( !data || typeof data !== "string" ) {
        return null;
    }
    if ( typeof context === "boolean" ) {
        keepScripts = context;
        context = false;
    }
    context = context || document;
    var parsed = rsingleTag.exec( data ),
        scripts = !keepScripts && [];
    // Single tag
    if ( parsed ) {
        return [ context.createElement( parsed[1] ) ];
    }
    parsed = jQuery.buildFragment( [ data ], context, scripts );
    if ( scripts ) {
        jQuery( scripts ).remove();
    }
    return jQuery.merge( [], parsed.childNodes );
},

匹配一個獨立的標籤

rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
  • ^<(\w+)\s*\/?>  : 以<開頭,至少跟著一個字元和任意個空白字元,之後出現0或1次/>
  • (?:<\/\1>|)$        : 可以匹配<、一個/或者空白並以之為結尾

      這樣如果沒有任何屬性和子節點的字串(比如'<html></html>'或者'<div></div>'這樣)會通過正則的匹配,當通過正則的匹配後則會通過傳入的上下文直接建立一個節點:

只是單一的標籤:

if ( parsed ) {
        return [ context.createElement( parsed[1] ) ];
 }

而未通過節點的字串,則通過建立一個div節點,將字串置入div的innerHTML:

parsed = jQuery.buildFragment( [ data ], context, scripts );

它會把傳入的複雜的html轉為文件碎片並且儲存在jQuery.fragments這個物件裡。這裡要提一下,document.createDocumentFragment()是相當好用的,可以減少對dom的操作.

建立一個文件碎片DocumentFragment

  • 如果要插入多個DOM元素,可以先將這些DOM元素插入一個文件碎片,然後將文件碎片插入文件中,這時插入的不是文件碎片,而是它的子孫節點;相比於挨個插入DOM元素,使用文件碎片可以獲得2-3倍的效能提升;
  • 如果將重複的HTML程式碼轉換為DOM元素,可以將轉換後的DOM元素快取起來,下次(實際是第3次)轉換同樣的HTML程式碼時,可以直接快取的DOM元素克隆返

當一個HTML比一個沒有屬性的簡單標籤複雜的時候,實際上,建立元素的處理是利用了瀏覽器的innerHTML 機制。

1 tmp = tmp || fragment.appendChild( context.createElement("div") );
2 
3 // Deserialize a standard representation
4 tag = ( rtagName.exec( elem ) || ["", ""] )[ 1 ].toLowerCase();
5 wrap = wrapMap[ tag ] || wrapMap._default;
6 tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1></$2>" ) + wrap[ 2 ];
  • 特別說明,jQuery建立一個新的<div>元素,並且設定innerHTML屬性為傳入的HTML程式碼片段。當引數是一個單標籤,就像 $('<img />') or $('<a></a>'),jQuery將使用javasrcipt原生的 createElement()函式建立這個元素。
  • 當傳入一個複雜的html,一些瀏覽器可能不會產生一個完全複製HTML原始碼所提供的DOM。正如前面提到的,jQuery使用的瀏覽器.innerHTML屬性來解析傳遞的HTML並將其插入到當前文件中。在此過程中,一些瀏覽器過濾掉某些元素,如<html>, <title>, 或 <head>的元素。其結果是,被插入元素可能不是傳入的原始的字串。
  • 不過,這些被過濾掉的標籤有限的。有些瀏覽器可能不完全複製所提供的HTML原始碼生成DOM。例如,Internet Explorer的版本8之前轉換所有連結的href屬性為絕對URL路徑,和Internet Explorer第9版之前,不增加一個單獨的相容層的情況下,將無法正確處理HTML5元素。
  • 為了確保跨平臺的相容性,程式碼片段必須是良好的。標籤可以包含其他元素,但需要搭配的結束標記

 如果第一個引數(HTML字串)為一個空的單標籤,且第二個引數context為一個非空純物件

var jqHTML = $('<div></div>', { class: 'css-class', data-name: 'data-val' }); 

console.log(jqHTML.attr['class']); //css-class 
console.log(jqHTML.attr['data-name']); //data-val 

匹配模式三:$(.className)

如果第一個引數是一個.className,jQuery物件中擁有class名為className的標籤元素,並增加一個屬性值為引數字串、document的selector、context屬性

return jQuery(document).find(className); 

匹配模式四:$(.className, context)

如果第一個引數是.className,第二個引數是一個上下文物件(可以是.className(等同於處理$(.className .className)),jQuery物件或dom節點), 
jQuery物件包含第二個引數上下文物件中擁有class名為className的後代節點元素,並增加一個context和selector屬性

return jQuery(context).find(className); 

匹配模式五:$(jQuery物件)

如果第一個引數是jQuery物件,上面已經分析過如果在查詢dom時,引數是一個#加元素id,返回的jQuery物件會增加一個屬性值為引數字串、document的selector、context屬性

var jq = $('#container'); 
console.log(jq.selector); // #container 
console.log(jq.context); // document 

那麼當出現$($('#container'))該如何處理呢?同樣的,返回的jQuery物件同情況

var jq2 = $($('#container')); 
console.log(jq2.selector); // #container 
console.log(jq2.context); // document 

等等..................

jQuery 構造器

     由此可見,從本質上來說,構建的jQuery物件,其實不僅僅只是dom,還有很多附加的元素,用陣列的方式儲存,當然各種組合有不一樣,但是儲存的方式是一樣的

總的來說分2大類:

  • 單個DOM元素,如$(ID),直接把DOM元素作陣列傳遞給this物件
  • 多個DOM元素,集合形式,可以通過CSS選擇器匹配是有的DOM元素,過濾操作,構建資料結構

CSS選擇器是通過jQuery.find(selector)函式完成的,通過它可以分析選擇器字串,並在DOM文件樹中查詢符合語法的元素集合

選擇器這章有點亂,東西太多了,不能一一陳列 , 後期在慢慢整理