您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關jQuery 2.0.3如何用源碼分析Sizzle引擎,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
什么是JavaScript的“預編譯”?
function Aaron() { alert("hello"); }; Aaron(); //這里調用Aaron,輸出world而不是hello function Aaron() { alert("world"); }; Aaron(); //這里調用Aaron,當然輸出world
按理說,兩個簽名完全相同的函數,在其他編程語言中應該是非法的。但在JavaScript中,這沒錯。不過,程序運行之后卻發現一個奇怪的現象:兩次調用都只是***那個函數里輸出的值!顯然***個函數沒有起到任何作用。這又是為什么呢?
JavaScript執行引擎并非一行一行地分析和執行程序,而是一段一段地進行預編譯后讓后 再執行的。而且,在同一段程序中,函數 在被執行之前 會被預定義,后定定義的 同名函數 會覆蓋 先定義的函數。在調用函數的時候,只會調用后一個預定義的函數(因為后一個預定義的函數把前一個預定義的函數覆蓋了)。也就是說,在***次調用myfunc之前,***個函數語句定義的代碼邏輯,已被第二個函數定義語句覆蓋了。所以,兩次都調用都是執行***一個函數邏輯了。
我們用實際證明下:
//***段代碼 <script> function Aaron() { alert("hello"); }; Aaron(); //hello </script> //第二段代碼 <script> function Aaron() { alert("world"); }; Aaron(); //world </script>
一段代碼中的定義式函數語句會優先執行,這似乎有點象靜態語言的編譯概念。所以,這一特征也被有些人稱為:JavaScript的“預編譯”
所以總結下:JS 解析器在執行語句前會將函數聲明和變量定義進行"預編譯",而這個"預編譯",并非一個頁面一個頁面地"預編譯",而是一段一段地預編譯,所謂的段就是一 個 <script> 塊。
那么我們再來看看
什么是編譯函數?
這個概念呢,我只用自己的語言表述下吧,先看看我在實際項目中的一種使用吧~
這里大概介紹下,偶做的是phonegap項目,基本實現了一套ppt的模板動畫
PPT的的功能設置(支持生成3個平臺的應用)
通過這個PPT直接描述出用戶行為的數據,然后直接打包生成相對應的實現應用了,實現部分是JS+CSS3+html5 ,關鍵是可以跨平臺哦
PC上的效果
頁面的元素都是動態的可運行可以交互的
編譯出來的的APK
通過一套PPT軟件生成的,頁面有大量的動畫,聲音,視頻,路徑動畫,交互,拖動 等等效果,這里不細說了,那么我引入編譯函數這個概念我是用來干什么事呢?
一套大的體系,流程控制是非常重要的,簡單的來說呢就是在某個階段該干哪一件事件了
但是JS呢其實就是一套異步編程的模型
編寫異步代碼是時常的事,比如有常見的異步操作:
Ajax(XMLHttpRequest)
Image Tag,Script Tag,iframe(原理類似)
setTimeout/setInterval
CSS3 Transition/Animation
HTML5 Web Database
postMessage
Web Workers
Web Sockets
and more…
JavaScript是一門單線程語言,因此一旦有某個API阻塞了當前線程,就相當于阻塞了整個程序,所以“異步”在JavaScript編程中占有很重要的地位。異步編程對程序執行效果的好處這里就不多談了,但是異步編程對于開發者來說十分麻煩,它會將程序邏輯拆分地支離破碎,語義完全丟失。因此,許多程序員都在打造一些異步編程模型已經相關的API來簡化異步編程工作,例如Promise模型
現在有的異步流程控制大多是基于CommonJS Promises規范,比如 jsdeferred,jQuery自己的deferred等等
從用戶角度來說呢,越是功能強大的庫,則往往意味著更多的API,以及更多的學習時間,這樣開發者才能根據自身需求選擇最合適的方法
從開發者角度,API的粒度問題,粒度越大的API往往功能越強,可以通過少量的調用完成大量工作,但粒度大往往意味著難以復用。越細粒度的API靈活度往往越高,可以通過有限的API組合出足夠的靈活性,但組合是需要付出“表現力”作為成本的。JavaScript在表現力方面有一些硬傷。
好像這里有點偏題了,總的來說呢,各種異步編程模型都是種抽象,它們是為了實現一些常用的異步編程模式而設計出來的一套有針對性的API。但是,在實際使用過程中我們可能遇到千變萬化的問題,一旦遇到模型沒有“正面應對”的場景,或是觸及這種模型的限制,開發人員往往就只能使用一些相對較為丑陋的方式來“回避問題”
那么在我們實際的開發中呢,我們用JS表達一段邏輯,由于在各種環境上存在著各種不同的異步情景,代碼執行流程會在這里“暫停”,等待該異步操作結束,然后再繼續執行后續代碼
如果是這樣的情況
var a = 1; setTimeout(function(){ a++; },1000) alert(a)//1
這段代碼很簡單,但是結果確不是我們想要的,我們修改一下
var a = 1; var b = function(callback) { setTimeout(function() { a++; callback(); }, 1000) } b(function(){ alert(a) //2 })
任何一個普通的JavaScript程序員都能順利理解這段代碼的含義,這里的“回調”并不是“阻塞”,而會空出執行線程,直至操作完成。而且,假如系統本身沒有提供阻塞的API,我們甚至沒有“阻塞”代碼的方法(當然,本就不該阻塞)。
到底編譯函數這個概念是干嘛?
JavaScript是單線程的,代碼也是同步從上向下執行的,執行流程不會隨便地暫停,當遇到異步的情況,從而改變了整個執行流程的時候,我們需要對代碼進行自動改寫,也就是在程序的執行過程中動態生成并執行新的代碼,這個過程我想稱之為編譯函數的一種運用吧.
我個人理解嘛,這里只是一個概念而已,閉包的一種表現方式,就像MVVM的angular就搞出一堆的概念,什么HTML編譯器,指令,表達式,依賴注入等等,當然是跟Javaer有關系…
這里回到我之前的項目上面,我個人引入這個編譯函數,是為了解決在流程中某個環節中因為異步導致的整個流程的執行出錯,所以在JS異步之后,我會把整個同步代碼編譯成一個閉包函數,因為這樣可以保留整個作用域的訪問,這樣等異步處理完畢之后,直接調用這個編譯函數進行匹配即可,這樣在異步的階段,同步的代碼也同時被處理了
其實說白了,就是一種閉包的使用,只是在不同的場景中換了一個優雅的詞匯罷了, 那么在sizzle中,引入這個編譯函數是解決什么問題了?
sizzle編譯函數
文章開頭就提到了,sizzle引入這個實現主要的作用是分詞的篩選,提高逐個匹配的效率
這里接著上一章節 解析原理
我們在經過詞法分析,簡單過濾,找到適合的種子集合之后
最終的選擇器抽出了input這個種子合集seed
重組的選擇器selector
div > p + div.aaron input[type="checkbox"]
還有詞法分析合集 group
Sizzle中的元匹配器
通過tokenize最終分類出來的group分別都有對應的幾種type
每一種type都會有對應的處理方法
Expr.filter = { ATTR : function (name, operator, check) { CHILD : function (type, what, argument, first, last) { CLASS : function (className) { ID : function (id) { PSEUDO : function (pseudo, argument) { TAG : function (nodeNameSelector) { }
可以把“元”理解為“原子”,也就是最小的那個匹配器。每條選擇器規則最小的幾個單元可以劃分為:ATTR | CHILD | CLASS | ID | PSEUDO | TAG
在Sizzle里邊有一些工廠方法用來生成對應的這些元匹配器,它就是Expr.filter。
舉2個例子(ID類型的匹配器由Expr.filter["ID"]生成,應該是判斷elem的id屬性跟目標屬性是否一致),
拿出2個源碼
//ID元匹配器工廠 Expr.filter["ID"] = function( id ) { var attrId = id.replace( runescape, funescape ); //生成一個匹配器, return function( elem ) { var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); //去除節點的id,判斷跟目標是否一致 return node && node.value === attrId; }; };
//屬性元匹配器工廠 //name :屬性名 //operator :操作符 //check : 要檢查的值 //例如選擇器 [type="checkbox"]中,name="type" operator="=" check="checkbox" "ATTR": function(name, operator, check) { //返回一個元匹配器 return function(elem) { //先取出節點對應的屬性值 var result = Sizzle.attr(elem, name); //看看屬性值有木有! if (result == null) { //如果操作符是不等號,返回真,因為當前屬性為空 是不等于任何值的 return operator === "!="; } //如果沒有操作符,那就直接通過規則了 if (!operator) { return true; } result += ""; //如果是等號,判斷目標值跟當前屬性值相等是否為真 return operator === "=" ? result === check : //如果是不等號,判斷目標值跟當前屬性值不相等是否為真 operator === "!=" ? result !== check : //如果是起始相等,判斷目標值是否在當前屬性值的頭部 operator === "^=" ? check && result.indexOf(check) === 0 : //這樣解釋: lang*=en 匹配這樣 <html lang="xxxxenxxx">的節點 operator === "*=" ? check && result.indexOf(check) > -1 : //如果是末尾相等,判斷目標值是否在當前屬性值的末尾 operator === "$=" ? check && result.slice(-check.length) === check : //這樣解釋: lang~=en 匹配這樣 <html lang="zh_CN en">的節點 operator === "~=" ? (" " + result + " ").indexOf(check) > -1 : //這樣解釋: lang=|en 匹配這樣 <html lang="en-US">的節點 operator === "|=" ? result === check || result.slice(0, check.length + 1) === check + "-" : //其他情況的操作符號表示不匹配 false; }; },
到這里應該想到Sizzle其實是不是就是通過對selector做“分詞”,打散之后再分別從Expr.filter 里面去找對應的方法來執行具體的查詢或者過濾的操作?
答案基本是肯定的
但是這樣常規的做法邏輯上是OK的,但是效率如何?
所以Sizzle有更具體和巧妙的做法
Sizzle在這里引入了 編譯函數的概念
通過Sizzle.compile方法內部的,
matcherFromTokens matcherFromGroupMatchers
把分析關系表,生成用于匹配單個選擇器群組的函數
matcherFromTokens,它充當了selector“分詞”與Expr中定義的匹配方法的串聯與紐帶的作用,可以說選擇符的各種排列組合都是能適應的了。Sizzle巧妙的就是它沒有直接將拿到的“分詞”結果與Expr中的方法逐個匹配逐個執行,而是先根據規則組合出一個大的匹配方法,***一步執行
我們看看如何用matcherFromTokens來生成對應Token的匹配器?
先貼源碼
Sizzle.compile
//編譯函數機制 //通過傳遞進來的selector和match生成匹配器: compile = Sizzle.compile = function(selector, group /* Internal Use Only */ ) { var i, setMatchers = [], elementMatchers = [], cached = compilerCache[selector + " "]; if (!cached) { //依舊看看有沒有緩存 // Generate a function of recursive functions that can be used to check each element if (!group) { //如果沒有詞法解析過 group = tokenize(selector); } i = group.length; //從后開始生成匹配器 //如果是有并聯選擇器這里多次等循環 while (i--) { //這里用matcherFromTokens來生成對應Token的匹配器 cached = matcherFromTokens(group[i]); if (cached[expando]) { setMatchers.push(cached); } else { //普通的那些匹配器都壓入了elementMatchers里邊 elementMatchers.push(cached); } } // Cache the compiled function // 這里可以看到,是通過matcherFromGroupMatchers這個函數來生成最終的匹配器 cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers)); } //把這個***匹配器返回到select函數中 return cached; };
matcherFromTokens
1: //生成用于匹配單個選擇器組的函數 2: //充當了selector“tokens”與Expr中定義的匹配方法的串聯與紐帶的作用, 3: //可以說選擇符的各種排列組合都是能適應的了 4: //Sizzle巧妙的就是它沒有直接將拿到的“分詞”結果與Expr中的方法逐個匹配逐個執行, 5: //而是先根據規則組合出一個大的匹配方法,***一步執行。但是組合之后怎么執行的 6: function matcherFromTokens(tokens) { 7: var checkContext, matcher, j, 8: len = tokens.length, 9: leadingRelative = Expr.relative[tokens[0].type], 10: implicitRelative = leadingRelative || Expr.relative[" "], //親密度關系 11: i = leadingRelative ? 1 : 0, 12: 13: // The foundational matcher ensures that elements are reachable from top-level context(s) 14: // 確保這些元素可以在context中找到 15: matchContext = addCombinator(function(elem) { 16: return elem === checkContext; 17: }, implicitRelative, true), 18: matchAnyContext = addCombinator(function(elem) { 19: return indexOf.call(checkContext, elem) > -1; 20: }, implicitRelative, true), 21: 22: //這里用來確定元素在哪個context 23: matchers = [ 24: function(elem, context, xml) { 25: return (!leadingRelative && (xml || context !== outermostContext)) || ( 26: (checkContext = context).nodeType ? 27: matchContext(elem, context, xml) : 28: matchAnyContext(elem, context, xml)); 29: } 30: ]; 31: 32: for (; i < len; i++) { 33: // Expr.relative 匹配關系選擇器類型 34: // "空 > ~ +" 35: if ((matcher = Expr.relative[tokens[i].type])) { 36: //當遇到關系選擇器時elementMatcher函數將matchers數組中的函數生成一個函數 37: //(elementMatcher利用了閉包所以matchers一直存在內存中) 38: matchers = [addCombinator(elementMatcher(matchers), matcher)]; 39: } else { 40: //過濾 ATTR CHILD CLASS ID PSEUDO TAG 41: matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches); 42: 43: // Return special upon seeing a positional matcher 44: //返回一個特殊的位置匹配函數 45: //偽類會把selector分兩部分 46: if (matcher[expando]) { 47: // Find the next relative operator (if any) for proper handling 48: // 發現下一個關系操作符(如果有話)并做適當處理 49: j = ++i; 50: for (; j < len; j++) { 51: if (Expr.relative[tokens[j].type]) { //如果位置偽類后面還有關系選擇器還需要篩選 52: break; 53: } 54: } 55: return setMatcher( 56: i > 1 && elementMatcher(matchers), 57: i > 1 && toSelector( 58: // If the preceding token was a descendant combinator, insert an implicit any-element `*` 59: tokens.slice(0, i - 1).concat({ 60: value: tokens[i - 2].type === " " ? "*" : "" 61: }) 62: ).replace(rtrim, "$1"), 63: matcher, 64: i < j && matcherFromTokens(tokens.slice(i, j)), //如果位置偽類后面還有選擇器需要篩選 65: j < len && matcherFromTokens((tokenstokens = tokens.slice(j))), //如果位置偽類后面還有關系選擇器還需要篩選 66: j < len && toSelector(tokens) 67: ); 68: } 69: matchers.push(matcher); 70: } 71: } 72: 73: return elementMatcher(matchers); 74: }
重點就是
cached = matcherFromTokens(group[i]);
cached 的結果就是matcherFromTokens返回的matchers編譯函數了
matcherFromTokens的分解是有規律的:
語義節點+關系選擇器的組合
div > p + div.aaron input[type="checkbox"]
Expr.relative 匹配關系選擇器類型
當遇到關系選擇器時elementMatcher函數將matchers數組中的函數生成一個函數
在遞歸分解tokens中的詞法元素時
提出***個typ匹配到對應的處理方法
matcher = Expr.filter[tokens[i].type].apply(null, tokens[i].matches); "TAG": function(nodeNameSelector) { var nodeName = nodeNameSelector.replace(runescape, funescape).toLowerCase(); return nodeNameSelector === "*" ? function() { return true; } : function(elem) { return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; }; },
matcher其實最終結果返回的就是bool值,但是這里返回只是一個閉包函數,不會馬上執行,這個過程換句話就是 編譯成一個匿名函數
繼續往下分解
如果遇到關系選著符就會合并分組了
matchers = [addCombinator(elementMatcher(matchers), matcher)];
通過elementMatcher生成一個***匹配器
function elementMatcher(matchers) { //生成一個***匹配器 return matchers.length > 1 ? //如果是多個匹配器的情況,那么就需要elem符合全部匹配器規則 function(elem, context, xml) { var i = matchers.length; //從右到左開始匹配 while (i--) { //如果有一個沒匹配中,那就說明該節點elem不符合規則 if (!matchers[i](elem, context, xml)) { return false; } } return true; } : //單個匹配器的話就返回自己即可 matchers[0]; }
看代碼大概就知道,就是分解這個子匹配器了,返回又一個curry函數,給addCombinator方法
//addCombinator方法就是為了生成有位置詞素的匹配器。 function addCombinator(matcher, combinator, base) { var dir = combinator.dir, checkNonElements = base && dir === "parentNode", donedoneName = done++; //第幾個關系選擇器 return combinator.first ? // Check against closest ancestor/preceding element // 檢查最靠近的祖先元素 // 如果是緊密關系的位置詞素 function(elem, context, xml) { while ((elemelem = elem[dir])) { if (elem.nodeType === 1 || checkNonElements) { //找到***個親密的節點,立馬就用***匹配器判斷這個節點是否符合前面的規則 return matcher(elem, context, xml); } } } : // Check against all ancestor/preceding elements //檢查最靠近的祖先元素或兄弟元素(概據>、~、+還有空格檢查) //如果是不緊密關系的位置詞素 function(elem, context, xml) { var data, cache, outerCache, dirkey = dirruns + " " + doneName; // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching // 我們不可以在xml節點上設置任意數據,所以它們不會從dir緩存中受益 if (xml) { while ((elemelem = elem[dir])) { if (elem.nodeType === 1 || checkNonElements) { if (matcher(elem, context, xml)) { return true; } } } } else { while ((elemelem = elem[dir])) { //如果是不緊密的位置關系 //那么一直匹配到true為止 //例如祖宗關系的話,就一直找父親節點直到有一個祖先節點符合規則為止 if (elem.nodeType === 1 || checkNonElements) { outerCache = elem[expando] || (elem[expando] = {}); //如果有緩存且符合下列條件則不用再次調用matcher函數 if ((cache = outerCache[dir]) && cache[0] === dirkey) { if ((data = cache[1]) === true || data === cachedruns) { return data === true; } } else { cache = outerCache[dir] = [dirkey]; cache[1] = matcher(elem, context, xml) || cachedruns; //cachedruns//正在匹配第幾個元素 if (cache[1] === true) { return true; } } } } } }; }
matcher為當前詞素前的“匹配器”
combinator為位置詞素
根據關系選擇器檢查
如果是這類沒有位置詞素的選擇器:’#id.aaron[name="checkbox"]‘
從右到左依次看看當前節點elem是否匹配規則即可。但是由于有了位置詞素,
那么判斷的時候就不是簡單判斷當前節點了,
可能需要判斷elem的兄弟或者父親節點是否依次符合規則。
這是一個遞歸深搜的過程。
所以matchers又經過一層包裝了
然后用同樣的方式遞歸下去,直接到tokens分解完畢
返回的結果一個根據關系選擇器分組后在組合的嵌套很深的閉包函數了
看看結構
但是組合之后怎么執行?
superMatcher方法是matcherFromGroupMatchers( elementMatchers, setMatchers )方法return出來的,但是執行起重要作用的是它
以上就是jQuery 2.0.3如何用源碼分析Sizzle引擎,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。