jQuery原始碼分析之Sizzle方法

antzone發表於2017-04-03

下面是jQuery原始碼關於Sizzle方法分析。

分析文字並沒有和原始碼分離,而是作為註釋的方式存在,比較便於閱讀。

程式碼如下:

[JavaScript] 純文字檢視 複製程式碼
/*
 * Sizzle方法是Sizzle選擇器包的主要入口,jQuery的find方法就是呼叫該方法獲取匹配的節點
 * 該方法主要完成下列任務:
 * 1、對於單一選擇器,且是ID、Tag、Class三種型別之一,則直接獲取並返回結果
 * 2、對於支援querySelectorAll方法的瀏覽器,通過執行querySelectorAll方法獲取並返回匹配的DOM元素
 * 3、除上之外則呼叫select方法獲取並返回匹配的DOM元素
 * 
 * 
 * @param selector 選擇器字串
 * @param context 執行匹配的最初的上下文(即DOM元素集合)。若context沒有賦值,則取document。
 * @param results 已匹配出的部分最終結果。若results沒有賦值,則賦予空陣列。
 * @param seed 初始集合
 */
function Sizzle(selector, context, results, seed) {
    var match, elem, m, nodeType,
    // QSA vars
    i, groups, old, nid, newContext, newSelector;
  
    /*
     * preferredDoc = window.document
     * 
     * setDocument方法完成一些初始化工作
     */
    if ((context ? context.ownerDocument || context : preferredDoc) !== document) {
        setDocument(context);
    }
  
    context = context || document;
    results = results || [];
  
    /*
     * 若selector不是有效地字串型別資料,則直接返回results
     */
    if (!selector || typeof selector !== "string") {
        return results;
    }
  
    /*
     * 若context既不是document(nodeType=9),也不是element(nodeType=1),那麼就返回空集合
     */
    if ((nodeType = context.nodeType) !== 1 && nodeType !== 9) {
        return [];
    }
  
    // 若當前過濾的是HTML文件,且沒有設定seed,則執行if內的語句體
    if (documentIsHTML && !seed) {
  
        /* 
         * 若選擇器是單一選擇器,且是ID、Tag、Class三種型別之一,則直接獲取並返回結果
         * 
         * rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/
         * 上述正規表示式括號內三段依次分別用來判斷是否是ID、TAG、CLASS型別的單一選擇器
         * 上述正規表示式在最外層圓括號內有三個子表示式(即三個圓括號括起來的部分),
         *   分別代表ID、Tag、Class選擇器的值,在下面程式碼中,分別體現在match[1]、match[2]、match[3]
         */
        if ((match = rquickExpr.exec(selector))) {
            // Speed-up: Sizzle("#ID")
            // 處理ID型別選擇器,如:#ID
            if ((m = match[1])) {
                // 若當前上下文是一個document,則執行if內語句體
                if (nodeType === 9) {
                    elem = context.getElementById(m);
                    // Check parentNode to catch when Blackberry 4.6
                    // returns
                    // nodes that are no longer in the document #6963
                    if (elem && elem.parentNode) {
                        // Handle the case where IE, Opera, and Webkit
                        // return items
                        // by name instead of ID
                        /*
                         * 一些老版本的瀏覽器會把name當作ID來處理,
                         * 返回不正確的結果,所以需要再一次對比返回節點的ID屬性
                         */
                        if (elem.id === m) {
                            results.push(elem);
                            return results;
                        }
                    } else {
                        return results;
                    }
                } else {
                    // Context is not a document
                    /*
                     * contains(context, elem)用來確認獲取的elem是否是當前context物件的子物件
                     */
                    if (context.ownerDocument
                            && (elem = context.ownerDocument.getElementById(m))
                            && contains(context, elem) && elem.id === m) {
                        results.push(elem);
                        return results;
                    }
                }
  
                // Speed-up: Sizzle("TAG")
                // 處理Tag型別選擇器,如:SPAN
            } else if (match[2]) {
                push.apply(results, context.getElementsByTagName(selector));
                return results;
  
                // Speed-up: Sizzle(".CLASS")
                /*
                 * 處理class型別選擇器,如:.class
                 * 下面條件判斷分別是:
                 * m = match[3]:有效的class型別選擇器
                 * support.getElementsByClassName 該選擇器的div支援getElementsByClassName
                 * context.getElementsByClassName 當前上下文節點有getElementsByClassName方法
                 * 
                 */
                  
            } else if ((m = match[3]) && support.getElementsByClassName
                    && context.getElementsByClassName) {
                push.apply(results, context.getElementsByClassName(m));
                return results;
            }
        }
  
        // QSA path
        /*
         * 若瀏覽器支援querySelectorAll方法且選擇器符合querySelectorAll呼叫標準,則執行if內語句體
         * 在這裡的檢查僅僅是簡單匹配
         * 第一次呼叫Sizzle時,rbuggyQSA為空
         * 
         * if語句體內對當前context物件的id的賦值與恢復,是用來修正querySelectorAll的一個BUG
         * 該BUG會在某些情況下把當前節點(context)也作為結果返回回來。
         * 具體方法是,在現有的選擇器前加上一個屬性選擇器:[id=XXX],
         * XXX 為context的id,若context本身沒有設定id,則給個預設值expando。
         */
          
        if (support.qsa && (!rbuggyQSA || !rbuggyQSA.test(selector))) {
            nid = old = expando;
            newContext = context;
            // 若context是document,則newSelector取自selector,否則為false
            newSelector = nodeType === 9 && selector;
  
            // qSA works strangely on Element-rooted queries
            // We can work around this by specifying an extra ID on the
            // root
            // and working up from there (Thanks to Andrew Dupont for
            // the technique)
            // IE 8 doesn't work on object elements
            if (nodeType === 1 && context.nodeName.toLowerCase() !== "object") {
                groups = tokenize(selector);
  
                if ((old = context.getAttribute("id"))) {
                    /*
                     * rescape = /'|\\/g,
                     * 這裡將old中的單引號、豎槓、反斜槓前加一個反斜槓
                     * old.replace(rescape, "\\$&")程式碼中的$&代表匹配項
                     */
                    nid = old.replace(rescape, "\\$&");
                } else {
                    context.setAttribute("id", nid);
                }
                nid = "[id='" + nid + "'] ";
  
                // 重新組合新的選擇器
                i = groups.length;
                while (i--) {
                  groups[i] = nid + toSelector(groups[i]);
                }
                /*
                 * rsibling = new RegExp(whitespace + "*[+~]")
                 * rsibling用於判定選擇器是否存在兄弟關係符
                 * 若包含+~符號,則取context的父節點作為當前節點
                 */
                newContext = rsibling.test(selector) && context.parentNode
                        || context;
                newSelector = groups.join(",");
            }
  
            if (newSelector) {
                /*
                 * 這裡之所以需要用try...catch,
                 * 是因為jquery所支援的一些選擇器是querySelectorAll所不支援的,
                 * 當使用這些選擇器時,querySelectorAll會報非法選擇器,
                 * 故需要jquery自身去實現。
                 */
                try {
                    // 將querySelectorAll獲取的結果併入results,而後返回resulsts
                    push.apply(results, newContext
                            .querySelectorAll(newSelector));
                    return results;
                } catch (qsaError) {
                } finally {
                    if (!old) {
                        context.removeAttribute("id");
                    }
                }
            }
        }
    }
  
    // All others
    // 除上述快捷方式和呼叫querySelectorAll方式直接獲取結果外,其餘都需呼叫select來獲取結果
    /*
     * rtrim = new RegExp("^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)"
     *          + whitespace + "+$", "g"),
     * whitespace = "[\\x20\\t\\r\\n\\f]";
     * 上述rtrim正規表示式的作用是去掉selector兩邊的空白,空白字元由whitespace變數定義
     * rtrim的效果與new RegExp("^" + whitespace + "+|" + whitespace + "+$", "g")相似
     */
    return select(selector.replace(rtrim, "$1"), context, results, seed);
}

相關文章