1. 程式人生 > >解密jQuery核心 Sizzle引擎篩選器

解密jQuery核心 Sizzle引擎篩選器

本章開始分析過濾器,根據API的順序來

主要涉及的知識點

jQuery的組成

pushStack方法的作用

sizzle偽類選擇器

首頁我們知道jQuery物件是一個數組物件

內部結構

image

jQuery的選擇最終還是依靠的DOM提供的介面,jQuery只是最了最佳的方式最快的匹配到合適的位置

構建一個基礎的jQuery物件有:

元素合集

元素數量

上下文

通過pushStack()方法構建的prevObject的引用儲存,這個在DOM操作的時候特別有用

選擇器

.eq( index )

如果一個jQuery物件表示一個DOM元素的集合,.eq()

方法從集合的一個元素中構造新的jQuery物件。所提供的索引標識這個集合中的元素的位置。

根據jQuery的結構,這個很好處理了,返回選擇器中的元素,陣列索引從0開始的

eq: function( i ) {
    var len = this.length,
        j = +i + ( i < 0 ? len : 0 );
    return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );
},

這裡有個細節,我們取DOM元素是索引,取出來的只是一個節點了,那麼還要實現鏈式操作,我們都知道返回this就能鏈式,

但是jQuery的每一次操作都是可以回溯的,包括節點的遍歷查詢,所以這時候返回的當前的this是不行的,這樣就需要構建一個新的上下文物件

li.eq(2).css('background-color', 'red');

所以取出來的DOM節點還要通過pushStack方法給包裝一次

.first()

first: function() {
            return this.eq( 0 );
        },

.last()

last: function() {
    return this.eq( -1 );
},

可見first,last方法都是一路貨,通過eq取索引罷了

.slice()

如果提供的jQuery代表了一組DOM元素,.slice()方法從匹配元素的子集中構造一個新的jQuery物件。

所提供的start索引標識的設定一個集合中的元素的位置;如果end被省略,這個元素之後的所有元素將包含在結果中

slice: function() {
    return this.pushStack( core_slice.apply( this, arguments ) );
},

其實就是

[].slice.apply(this,arguments )

這裡的this是jq物件,根據引數轉成陣列子集

.map( callback(index, domElement) )

描述: 通過一個函式匹配當前集合中的每個元素,產生一個包含新的jQuery物件。

map有點類似each的趕腳,本質上還是有區別的

首先:

在設計上each是不可改變的迭代器,each方法就相當於js中的for迴圈,返回 'false' 將停止迴圈 (就像在普通的迴圈中使用 'break')

map的方法可以作為一個迭代器,但實際是為了操縱提供的陣列並返回一個新陣列。

map將一組元素轉換成其他陣列(不論是否是元素陣列),你可以用這個函式來通過新規則(如過濾掉數字)來建立一個新列表,

不論是值、屬性還是CSS樣式,或者其他特別形式。這都可以用'$.map()'來方便的建立。

可見map除了迭代的功能還要返回一新的物件,所以潛在的有記憶體消耗

map: function( callback ) {
    return this.pushStack( jQuery.map(this, function( elem, i ) {
        return callback.call( elem, i, elem );
    }));
},
jQuery.map: function( elems, callback, arg ) {
            var value,
                i = 0,
                length = elems.length,
                isArray = isArraylike( elems ),
                ret = [];

            // Go through the array, translating each of the items to their
            if ( isArray ) {
                for ( ; i < length; i++ ) {
                    value = callback( elems[ i ], i, arg );

                    if ( value != null ) {
                        ret[ ret.length ] = value;
                    }
                }

                // Go through every key on the object,
            } else {
                for ( i in elems ) {
                    value = callback( elems[ i ], i, arg );

                    if ( value != null ) {
                        ret[ ret.length ] = value;
                    }
                }
            }

            // Flatten any nested arrays
            return core_concat.apply( [], ret );
        },

可見除了執行回撥,還要收集返回值

value = callback( elems[ i ], i, arg );

if ( value != null ) {
    ret[ ret.length ] = value;
}

通過jQuery.map會返回一個新的迭代物件,然後又讓pushStack方法包裝一個新的堆上的元素集合(jQuery物件)

.filter()

選擇器是個比較複雜的東東了,這裡又要涉及到sizzle的處理了,順便一起回顧以往的知識點,加以鞏固

講過濾器,就裡不得不提一下.find(),雖然2者都是一個效果,但是處理有本質的區別

jQuery官方的API這樣說明filterfind函式:

filter(selector):  
     Description: Reduce the set of matched elements to those that match the selector or pass the function’s test.  

find(selector):  
    Description: Get the descendants of each element in the current set of matched elements, filtered by a selector.

find()會在當前指定元素中查詢符合條件的子元素,是對它的子集操作,

filter()則是在當前指定的元素集合中查詢符合條件的元素,是對自身集合元素進行篩選。

顯而易見find()是對它的子集操作,filter()對自身集合元素篩選

find以後再說,先看看filter的實現

filter例項

官方給出的直接搬過來

<ul>
    <li>list item 1</li>
    <li>list item 2</li>
    <li>list item 3</li>
</ul>
li.filter(':even').css('background-color', 'red');

image

filter原始碼

第一步

可見winnow一定是返回一個數據物件,在通過pushStack包裝

filter: function( selector ) {
    return this.pushStack( winnow(this, selector || [], false) );
},

第二步

jQuery.extend({
    filter: function( expr, elems, not ) {
        var elem = elems[ 0 ];

        if ( not ) {
            expr = ":not(" + expr + ")";
        }

        return elems.length === 1 && elem.nodeType === 1 ?
            jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] :
            jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
                return elem.nodeType === 1;
            }));
    },

filter是針對元素節點本身操作的,所以還要顧慮一下保證合集中都是elem.nodeType === 1的節點型別才行

第三步

方法最終還是靠sizzle去解析,傳遞一個符合解析的實參

image

傳遞了選擇器與種子合集了,這裡具體可以參考之前的sizzle系列文章,走的流程依然一樣

通過詞法分析器,解析出詞法關係

image

Token:{  
   value:'匹配到的字串', 
   type:'對應的Token型別', 
   matches:'正則匹配到的一個結構'
}

幾種Token : TAG, ID, CLASS, ATTR, CHILD, PSEUDO, NAME,但是這裡的type對應是一種偽類PSEUDO,這是在之前sizzle裡面沒有提到的

sizzle偽類

其實之前的文章分析了tokenize方法,把選擇語句分解成分詞

當然看這裡需要結合之前的sizzle系列的篇幅了,比較複雜

Sizzle巧妙的就是它沒有直接將拿到的“分詞”結果與Expr中的方法逐個匹配逐個執行,而是先根據規則組合出一個大的匹配方法,最後一步執行

編譯函式機制中最核心的一段

Sizzle.compile:

i = group.length;
while ( i-- ) {
    cached = matcherFromTokens( group[i] );
    if ( cached[ expando ] ) {
        setMatchers.push( cached );
    } else {
        elementMatchers.push( cached );
    }
}
// Cache the compiled function
cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );

1 :matcherFromTokens方法,通過tokens生成匹配程式,它充當了selector“分詞”與Expr中定義的匹配方法的串聯與紐帶的作用,可以說選擇符的各種排列組合都是能適應的了

2: matcherFromGroupMatchers方法,通過返回curry的superMatcher方法執行

偽類如何生成最終的匹配器

matcherFromTokens原始碼核心部分

for ( ; i < len; i++ ) {
    if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
        matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
    } else {
        matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );

可見根據分詞器型別type的不同,會適配2組不同的處理方案

第一種是位置詞素,之前就講過,主要看下面一種

用Expr.filter的工廠方法來生成匹配器

每條選擇器規則最小的幾個單元可以劃分為:ATTR | CHILD | CLASS | ID | PSEUDO | TAG

image

位置偽元素

image

每個子規則都有對應的匹配器,同樣道理,位置偽類也有特殊的匹配器,它是由setMatcher工廠生成。

為了區分其他規則跟位置偽類,需要對位置偽類的過濾器匹配器等打個標記。

Sizzle原始碼裡邊用到的打標記方法

function markFunction( fn ) {
  fn[ expando ] = true;
  return fn;
}

位置偽元素共同的特點被createPositionalPseudo給curry一次了

function createPositionalPseudo(fn) {
    return markFunction(function(argument) {
        argument = +argument;
        return markFunction(function(seed, matches) {
            var j,
                matchIndexes = fn([], seed.length, argument),
                i = matchIndexes.length;

            // Match elements found at the specified indexes
            while (i--) {
                if (seed[(j = matchIndexes[i])]) {
                    seed[j] = !(matches[j] = seed[j]);
                }
            }
        });
    });
}

curry化 :它是一種通過把多個引數填充到函式體中,實現將函式轉換成一個新的經過簡化的(使之接受的引數更少)函式技術

當然也會形成閉包了,儲存私有值在作用

所以這裡很好理解,返回的函式其實最終是

function(argument) {
        argument = +argument;
        return markFunction(function(seed, matches) {
            var j,
                matchIndexes = fn([], seed.length, argument),
                i = matchIndexes.length;

            // Match elements found at the specified indexes
            while (i--) {
                if (seed[(j = matchIndexes[i])]) {
                    seed[j] = !(matches[j] = seed[j]);
                }
            }
        });
    });

所以總結,針對(位置型的)偽篩選:first/:last/:eq/:even/:odd/:It/:gt,都是做了單獨的處理,

然後過濾器還有別的比如:not,:has,:contains 等等,其實基礎的流程處理都差不多,只是針對不通的情況分別hack

在初始化的時候就:

1 返回新的curry函式

2 打上標記markFunction

回到主線方法:

Expr.filter.PSEUDO

"PSEUDO": function( pseudo, argument ) {
    // pseudo-class names are case-insensitive
    // http://www.w3.org/TR/selectors/#pseudo-classes
    // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
    // Remember that setFilters inherits from pseudos
    var args,
        fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
            Sizzle.error( "unsupported pseudo: " + pseudo );

    // The user may use createPseudo to indicate that
    // arguments are needed to create the filter function
    // just as Sizzle does
    if ( fn[ expando ] ) {
        return fn( argument );
    }

The user may use createPseudo to indicate that

其實顯而易見了,fn執行的正是上面

"even": createPositionalPseudo(function( matchIndexes, length ) {
    var i = 0;
    for ( ; i < length; i += 2 ) {
        matchIndexes.push( i );
    }
    return matchIndexes;
}),

執行繼續巢狀形成的curry 函數了,在返回一個curry方法,給新方法繼續打上一個標記

function( seed, matches ) {
    var j,
        matchIndexes = fn( [], seed.length, argument ),
        i = matchIndexes.length;

    // Match elements found at the specified indexes
    while ( i-- ) {
        if ( seed[ (j = matchIndexes[i]) ] ) {
            seed[j] = !(matches[j] = seed[j]);
        }
    }
}

所以最終的匹配器方法賊噁心的,嵌套了4,5層作用域

matcher :

image

為什麼要寫這麼複雜?因為可以合併不同的引數傳遞

其實sizzle就是核心處理機制是不變的,只是針對不同的分支做了不同的處理方式,當然寫東西整合在一起就顯得尤為的複雜了

下一章在深入位置偽類特有的 setMatcher 匹配器工廠