標籤:
最近在拜讀艾倫在慕課網上寫的JQuery課程,感覺在國內對JQuery程式碼分析透徹的人沒幾個能比得過艾倫。有沒有吹牛?是不是我說大話了?
什麼是Sizzle引擎?
我們經常使用JQuery的選取器查詢元素,查詢的選取器有簡單也有複雜:
簡單點:“div”、“.navi”、“div.navi”。
複雜點:"div input[type=‘checkbox‘]"、"div.navi + .section p"。
Query實現查詢時也是優先使用DOM標準查詢函數,例如:
document.getElementById()
document.getElementsByTagName()
document.getElementsByClassName()
document.getElementsByName()
進階瀏覽器還實現了:
querySelector()
querySelectorAll()
由於瀏覽器版本差異導致的相容問題,上面的函數並不是所有瀏覽器都支援。但JQuery得解決這些問題,所以就引入了Sizzle引擎。
JQuery在篩選元素時優先使用瀏覽器內建的進階查詢函數,因為查詢效率高。其次才選擇使用Sizzle引擎篩選元素。
Sizzle引擎的目的是根據傳入的selector選取器篩選出元素集合。執行過程經過詞法分析、編譯過程。通過詞法分析把一個selector字串分解成結構化的資料以便編譯過程使用。編譯過程充分利用了Javascript的閉包功能,產生一個函數鏈,在最終匹配時再去執行這個函數鏈。
舉個例子,一個選取器selector的值為”Aaron input[name=ttt]”,通過詞法分析,得到一個結構化數組:
[ { matches: ["div"], type: "TAG", value: "Aaron" }, { type: " ", value: " " }, { matches: ["name", "=", "ttt"], type: "ATTR", value: "[name=ttt]" }]
selector中的input作為一個種子集合seed。意思是Sizzle根據input查詢出所有input元素,結果存放到seed集合,編譯過程都是在seed集合中查詢過濾。
上面說的很粗糙,不便於理解,接下來我們就拿代碼來介紹。
通過程式碼分析原理
申明:下面的代碼來源於Aaron在慕課網上的Jquery教程
compile
/** * 編譯過程 */function compile(){ var seed = document.querySelectorAll("input"), selector = "Aaron [name=ttt]", elementMatchers = [], match = [ { matches: ["div"], type: "TAG", value: "Aaron" }, { type: " ", value: " " }, { matches: ["name", "=", "ttt"], type: "ATTR", value: "[name=ttt]" } ]; elementMatchers.push(matcherFromTokens(match)); //超級匹配器 var cached = matcherFromGroupMatchers(elementMatchers); var results = cached(seed); results[0].checked = ‘checked‘;}
JQuery的compile函數包含了所有的執行過程,由於本篇介紹的重點是編譯過程,所以詞法分析的過程未包含,這裡直接寫了match結果,實際JQuery會調用tokenize()函數擷取片語。
函數中調用了兩個函數:matcherFromTokens()和matcherFromGroupMatchers()。
matcherFromTokens():返回的是一個函數,函數結構如下:
返回函數格式為function(elem, context, xml),並且這個函數返回一個bool值表示elem是否匹配有效。
matcherFromGroupMatchers():函數代碼很簡單,遍曆seed集合,每個元素都調用elementMatcher函數。最終返回一個匹配成功的元素集合。
由於matcherFromGroupMatchers()函數比較簡單,所以就先介紹它。
matcherFromGroupMatchers
function matcherFromGroupMatchers(elmentMatchers){ return function(seed){ var results = []; var matcher, elem; for(var i = 0; i < seed.length; i++){ var elem = seed[i]; matcher = elmentMatchers[0]; if(matcher(elem)){ results.push(elem); } } return results; }}
遍曆seed元素,每一個元素都調用matcher函數,返回true則添加到results數組中。
matcherFromTokens
function matcherFromTokens(tokens){ var len = tokens.length, matcher, matchers = []; for(var i = 0; i < len; i++){ if(tokens[i].type === " "){ matchers = [addCombinator(elementMatcher(matchers))]; }else{ matcher = filter[tokens[i].type].apply(null, tokens[i].matches); matchers.push(matcher); } } return elementMatcher(matchers);}
整個編譯的核心也就在matcherFromTokens函數中,遍曆分詞tokens數組,分詞分兩大類,關係型和非關係型。關係型包括:“ ”、“>”、“+”、“~”。 剩下的都是非關係型分詞。
每一個非關係型分詞都會對應一個matcher:
第一個分詞類型為TAG,在filter中找到matcher。第二個分詞為關係分詞,調用addCombinator合并之前的matcher。第三個分詞類型為ATTR,在filter中找到matcher。最終matchers的值為:
在return的時候又調用了elementMatcher()函數,返回的結果還是一個函數。上面介紹compile函數時看到過返回的函數結構。
matcherFromTokens函數體中有用到addCombinator()和elementMatcher()函數以及filter對象。先看filter:
var filter = { ATTR: function(name, operator,check){ return function(elem){ var attr = elem.getAttribute(name); if(operator === "="){ if(attr === check){ return true; } } return false; } }, TAG: function(nodeNameSelector){ return function(elem){ return elem.nodeName && elem.nodeName.toLowerCase() === nodeNameSelector; } }}
一目瞭然,一看就知道filter中的ATTR和TAG對應了match分片語中的type類型,所以filter對應了非關係型分詞的matcher函數。
addCombinator
function addCombinator(matcher){ return function(elem, context, xml){ while(elem = elem["parentNode"]){ if(elem.nodeType === 1) //找到第一個親密的節點,立馬就用終極匹配器判斷這個節點是否符合前面的規則 return matcher(elem); } }}
addCombinator對應關係型分詞matcher。本例只列舉了祖先和子孫關係" "的合并,返回的結果是一個簽名為function(elem, contenxt, xml)的函數,函數中elem[“parentNode”]找到文件項目類型的父節點,再調用matcher校正這個父節點是否匹配。
所以關係型matcher函數執行的過程:先通過關聯類型找到匹配元素,然後再調用matcher校正匹配結果。
elementMatcher
function elementMatcher(matchers){ return matchers.length > 1 ? function(elem, context, xml){ var i = matchers.length; while(i--){ if(!matchers[i](elem, context, xml)){ return false; } } return true; }: matchers[0];}
elementMatcher()函數就是幹實事的傢伙,它遍曆matchers函數數組,執行每個matcher函數,一旦有matchers[i]返回false則整個匹配就失敗了。這裡需要注意的一點是i--,為什麼是反序遍曆?因為JQuery Sizzle匹配的原則是從右往左。由於前面的match數組是按照選取器從左往右儲存的,所以這裡先執行最後面的。
上面所有的代碼只是簡單類比了JQuery Sizzle引擎的執行過程,真實的原始碼很複雜,估計只有大神些才能領悟透徹。大神,艾倫得算一個。
說到閉包,如果能把JQuery Sizzle程式碼分析透徹,理解閉包易如反掌。本篇介紹的函數傳回值都是函數,而每個返回函數需要的變數都是通過閉包儲存起來,在真正執行函數的時候再讀取這些變數。
如果本篇內容對大家有協助,請點擊頁面右下角的關注。如果覺得不好,也歡迎拍磚。你們的評價就是博主的動力!下篇內容,敬請期待!
JQuery Sizzle引擎原始碼分析