jQuery 2.0.3源碼分析Sizzle引擎
什么是JavaScript的“預(yù)編譯”?
- function Aaron() {
- alert("hello");
- };
- Aaron(); //這里調(diào)用Aaron,輸出world而不是hello
- function Aaron() {
- alert("world");
- };
- Aaron(); //這里調(diào)用Aaron,當然輸出world
按理說,兩個簽名完全相同的函數(shù),在其他編程語言中應(yīng)該是非法的。但在JavaScript中,這沒錯。不過,程序運行之后卻發(fā)現(xiàn)一個奇怪的現(xiàn)象:兩次調(diào)用都只是***那個函數(shù)里輸出的值!顯然***個函數(shù)沒有起到任何作用。這又是為什么呢?
JavaScript執(zhí)行引擎并非一行一行地分析和執(zhí)行程序,而是一段一段地進行預(yù)編譯后讓后 再執(zhí)行的。而且,在同一段程序中,函數(shù) 在被執(zhí)行之前 會被預(yù)定義,后定定義的 同名函數(shù) 會覆蓋 先定義的函數(shù)。在調(diào)用函數(shù)的時候,只會調(diào)用后一個預(yù)定義的函數(shù)(因為后一個預(yù)定義的函數(shù)把前一個預(yù)定義的函數(shù)覆蓋了)。也就是說,在***次調(diào)用myfunc之前,***個函數(shù)語句定義的代碼邏輯,已被第二個函數(shù)定義語句覆蓋了。所以,兩次都調(diào)用都是執(zhí)行***一個函數(shù)邏輯了。
我們用實際證明下:
- //***段代碼
- <script>
- function Aaron() {
- alert("hello");
- };
- Aaron(); //hello
- </script>
- //第二段代碼
- <script>
- function Aaron() {
- alert("world");
- };
- Aaron(); //world
- </script>
一段代碼中的定義式函數(shù)語句會優(yōu)先執(zhí)行,這似乎有點象靜態(tài)語言的編譯概念。所以,這一特征也被有些人稱為:JavaScript的“預(yù)編譯”
所以總結(jié)下:JS 解析器在執(zhí)行語句前會將函數(shù)聲明和變量定義進行"預(yù)編譯",而這個"預(yù)編譯",并非一個頁面一個頁面地"預(yù)編譯",而是一段一段地預(yù)編譯,所謂的段就是一 個 <script> 塊。
那么我們再來看看
什么是編譯函數(shù)?
這個概念呢,我只用自己的語言表述下吧,先看看我在實際項目中的一種使用吧~
這里大概介紹下,偶做的是phonegap項目,基本實現(xiàn)了一套ppt的模板動畫
PPT的的功能設(shè)置(支持生成3個平臺的應(yīng)用)
通過這個PPT直接描述出用戶行為的數(shù)據(jù),然后直接打包生成相對應(yīng)的實現(xiàn)應(yīng)用了,實現(xiàn)部分是JS+CSS3+html5 ,關(guān)鍵是可以跨平臺哦
PC上的效果
頁面的元素都是動態(tài)的可運行可以交互的
移動端的效果
編譯出來的的APK
通過一套PPT軟件生成的,頁面有大量的動畫,聲音,視頻,路徑動畫,交互,拖動 等等效果,這里不細說了,那么我引入編譯函數(shù)這個概念我是用來干什么事呢?
一套大的體系,流程控制是非常重要的,簡單的來說呢就是在某個階段該干哪一件事件了
但是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編程中占有很重要的地位。異步編程對程序執(zhí)行效果的好處這里就不多談了,但是異步編程對于開發(fā)者來說十分麻煩,它會將程序邏輯拆分地支離破碎,語義完全丟失。因此,許多程序員都在打造一些異步編程模型已經(jīng)相關(guān)的API來簡化異步編程工作,例如Promise模型
現(xiàn)在有的異步流程控制大多是基于CommonJS Promises規(guī)范,比如 jsdeferred,jQuery自己的deferred等等
從用戶角度來說呢,越是功能強大的庫,則往往意味著更多的API,以及更多的學(xué)習(xí)時間,這樣開發(fā)者才能根據(jù)自身需求選擇最合適的方法
從開發(fā)者角度,API的粒度問題,粒度越大的API往往功能越強,可以通過少量的調(diào)用完成大量工作,但粒度大往往意味著難以復(fù)用。越細粒度的API靈活度往往越高,可以通過有限的API組合出足夠的靈活性,但組合是需要付出“表現(xiàn)力”作為成本的。JavaScript在表現(xiàn)力方面有一些硬傷。
好像這里有點偏題了,總的來說呢,各種異步編程模型都是種抽象,它們是為了實現(xiàn)一些常用的異步編程模式而設(shè)計出來的一套有針對性的API。但是,在實際使用過程中我們可能遇到千變?nèi)f化的問題,一旦遇到模型沒有“正面應(yīng)對”的場景,或是觸及這種模型的限制,開發(fā)人員往往就只能使用一些相對較為丑陋的方式來“回避問題”
那么在我們實際的開發(fā)中呢,我們用JS表達一段邏輯,由于在各種環(huán)境上存在著各種不同的異步情景,代碼執(zhí)行流程會在這里“暫停”,等待該異步操作結(jié)束,然后再繼續(xù)執(zhí)行后續(xù)代碼
如果是這樣的情況
- var a = 1; setTimeout(function(){ a++; },1000) alert(a)//1
這段代碼很簡單,但是結(jié)果確不是我們想要的,我們修改一下
- var a = 1; var b = function(callback) { setTimeout(function() { a++; callback(); }, 1000) } b(function(){ alert(a) //2 })
任何一個普通的JavaScript程序員都能順利理解這段代碼的含義,這里的“回調(diào)”并不是“阻塞”,而會空出執(zhí)行線程,直至操作完成。而且,假如系統(tǒng)本身沒有提供阻塞的API,我們甚至沒有“阻塞”代碼的方法(當然,本就不該阻塞)。
到底編譯函數(shù)這個概念是干嘛?
JavaScript是單線程的,代碼也是同步從上向下執(zhí)行的,執(zhí)行流程不會隨便地暫停,當遇到異步的情況,從而改變了整個執(zhí)行流程的時候,我們需要對代碼進行自動改寫,也就是在程序的執(zhí)行過程中動態(tài)生成并執(zhí)行新的代碼,這個過程我想稱之為編譯函數(shù)的一種運用吧.
我個人理解嘛,這里只是一個概念而已,閉包的一種表現(xiàn)方式,就像MVVM的angular就搞出一堆的概念,什么HTML編譯器,指令,表達式,依賴注入等等,當然是跟Javaer有關(guān)系…
這里回到我之前的項目上面,我個人引入這個編譯函數(shù),是為了解決在流程中某個環(huán)節(jié)中因為異步導(dǎo)致的整個流程的執(zhí)行出錯,所以在JS異步之后,我會把整個同步代碼編譯成一個閉包函數(shù),因為這樣可以保留整個作用域的訪問,這樣等異步處理完畢之后,直接調(diào)用這個編譯函數(shù)進行匹配即可,這樣在異步的階段,同步的代碼也同時被處理了
其實說白了,就是一種閉包的使用,只是在不同的場景中換了一個優(yōu)雅的詞匯罷了, 那么在sizzle中,引入這個編譯函數(shù)是解決什么問題了?
sizzle編譯函數(shù)
文章開頭就提到了,sizzle引入這個實現(xiàn)主要的作用是分詞的篩選,提高逐個匹配的效率
這里接著上一章節(jié) 解析原理
我們在經(jīng)過詞法分析,簡單過濾,找到適合的種子集合之后
最終的選擇器抽出了input這個種子合集seed
重組的選擇器selector
- div > p + div.aaron input[type="checkbox"]
還有詞法分析合集 group
Sizzle中的元匹配器
通過tokenize最終分類出來的group分別都有對應(yīng)的幾種type
每一種type都會有對應(yīng)的處理方法
- 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) { }
可以把“元”理解為“原子”,也就是最小的那個匹配器。每條選擇器規(guī)則最小的幾個單元可以劃分為:ATTR | CHILD | CLASS | ID | PSEUDO | TAG
在Sizzle里邊有一些工廠方法用來生成對應(yīng)的這些元匹配器,它就是Expr.filter。
舉2個例子(ID類型的匹配器由Expr.filter["ID"]生成,應(yīng)該是判斷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");
- //去除節(jié)點的id,判斷跟目標是否一致
- return node && node.value === attrId;
- };
- };
- //屬性元匹配器工廠
- //name :屬性名
- //operator :操作符
- //check : 要檢查的值
- //例如選擇器 [type="checkbox"]中,name="type" operator="=" check="checkbox"
- "ATTR": function(name, operator, check) {
- //返回一個元匹配器
- return function(elem) {
- //先取出節(jié)點對應(yīng)的屬性值
- var result = Sizzle.attr(elem, name);
- //看看屬性值有木有!
- if (result == null) {
- //如果操作符是不等號,返回真,因為當前屬性為空 是不等于任何值的
- return operator === "!=";
- }
- //如果沒有操作符,那就直接通過規(guī)則了
- if (!operator) {
- return true;
- }
- result += "";
- //如果是等號,判斷目標值跟當前屬性值相等是否為真
- return operator === "=" ? result === check :
- //如果是不等號,判斷目標值跟當前屬性值不相等是否為真
- operator === "!=" ? result !== check :
- //如果是起始相等,判斷目標值是否在當前屬性值的頭部
- operator === "^=" ? check && result.indexOf(check) === 0 :
- //這樣解釋: lang*=en 匹配這樣 <html lang="xxxxenxxx">的節(jié)點
- operator === "*=" ? check && result.indexOf(check) > -1 :
- //如果是末尾相等,判斷目標值是否在當前屬性值的末尾
- operator === "$=" ? check && result.slice(-check.length) === check :
- //這樣解釋: lang~=en 匹配這樣 <html lang="zh_CN en">的節(jié)點
- operator === "~=" ? (" " + result + " ").indexOf(check) > -1 :
- //這樣解釋: lang=|en 匹配這樣 <html lang="en-US">的節(jié)點
- operator === "|=" ? result === check || result.slice(0, check.length + 1) === check + "-" :
- //其他情況的操作符號表示不匹配
- false;
- };
- },
到這里應(yīng)該想到Sizzle其實是不是就是通過對selector做“分詞”,打散之后再分別從Expr.filter 里面去找對應(yīng)的方法來執(zhí)行具體的查詢或者過濾的操作?
答案基本是肯定的
但是這樣常規(guī)的做法邏輯上是OK的,但是效率如何?
所以Sizzle有更具體和巧妙的做法
Sizzle在這里引入了 編譯函數(shù)的概念
通過Sizzle.compile方法內(nèi)部的,
matcherFromTokens matcherFromGroupMatchers
把分析關(guān)系表,生成用于匹配單個選擇器群組的函數(shù)
matcherFromTokens,它充當了selector“分詞”與Expr中定義的匹配方法的串聯(lián)與紐帶的作用,可以說選擇符的各種排列組合都是能適應(yīng)的了。Sizzle巧妙的就是它沒有直接將拿到的“分詞”結(jié)果與Expr中的方法逐個匹配逐個執(zhí)行,而是先根據(jù)規(guī)則組合出一個大的匹配方法,***一步執(zhí)行
我們看看如何用matcherFromTokens來生成對應(yīng)Token的匹配器?
先貼源碼
Sizzle.compile
- //編譯函數(shù)機制
- //通過傳遞進來的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; //從后開始生成匹配器
- //如果是有并聯(lián)選擇器這里多次等循環(huán)
- while (i--) {
- //這里用matcherFromTokens來生成對應(yīng)Token的匹配器
- cached = matcherFromTokens(group[i]);
- if (cached[expando]) {
- setMatchers.push(cached);
- } else { //普通的那些匹配器都壓入了elementMatchers里邊
- elementMatchers.push(cached);
- }
- }
- // Cache the compiled function
- // 這里可以看到,是通過matcherFromGroupMatchers這個函數(shù)來生成最終的匹配器
- cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers));
- }
- //把這個***匹配器返回到select函數(shù)中
- return cached;
- };
matcherFromTokens
- 1: //生成用于匹配單個選擇器組的函數(shù)
- 2: //充當了selector“tokens”與Expr中定義的匹配方法的串聯(lián)與紐帶的作用,
- 3: //可以說選擇符的各種排列組合都是能適應(yīng)的了
- 4: //Sizzle巧妙的就是它沒有直接將拿到的“分詞”結(jié)果與Expr中的方法逐個匹配逐個執(zhí)行,
- 5: //而是先根據(jù)規(guī)則組合出一個大的匹配方法,***一步執(zhí)行。但是組合之后怎么執(zhí)行的
- 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[" "], //親密度關(guān)系
- 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 匹配關(guān)系選擇器類型
- 34: // "空 > ~ +"
- 35: if ((matcher = Expr.relative[tokens[i].type])) {
- 36: //當遇到關(guān)系選擇器時elementMatcher函數(shù)將matchers數(shù)組中的函數(shù)生成一個函數(shù)
- 37: //(elementMatcher利用了閉包所以matchers一直存在內(nèi)存中)
- 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: //返回一個特殊的位置匹配函數(shù)
- 45: //偽類會把selector分兩部分
- 46: if (matcher[expando]) {
- 47: // Find the next relative operator (if any) for proper handling
- 48: // 發(fā)現(xiàn)下一個關(guān)系操作符(如果有話)并做適當處理
- 49: j = ++i;
- 50: for (; j < len; j++) {
- 51: if (Expr.relative[tokens[j].type]) { //如果位置偽類后面還有關(guān)系選擇器還需要篩選
- 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))), //如果位置偽類后面還有關(guān)系選擇器還需要篩選
- 66: j < len && toSelector(tokens)
- 67: );
- 68: }
- 69: matchers.push(matcher);
- 70: }
- 71: }
- 72:
- 73: return elementMatcher(matchers);
- 74: }
重點就是
- cached = matcherFromTokens(group[i]);
cached 的結(jié)果就是matcherFromTokens返回的matchers編譯函數(shù)了
matcherFromTokens的分解是有規(guī)律的:
語義節(jié)點+關(guān)系選擇器的組合
- div > p + div.aaron input[type="checkbox"]
Expr.relative 匹配關(guān)系選擇器類型
當遇到關(guān)系選擇器時elementMatcher函數(shù)將matchers數(shù)組中的函數(shù)生成一個函數(shù)
在遞歸分解tokens中的詞法元素時
提出***個typ匹配到對應(yīng)的處理方法
- 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其實最終結(jié)果返回的就是bool值,但是這里返回只是一個閉包函數(shù),不會馬上執(zhí)行,這個過程換句話就是 編譯成一個匿名函數(shù)
繼續(xù)往下分解
如果遇到關(guān)系選著符就會合并分組了
- matchers = [addCombinator(elementMatcher(matchers), matcher)];
通過elementMatcher生成一個***匹配器
- function elementMatcher(matchers) {
- //生成一個***匹配器
- return matchers.length > 1 ?
- //如果是多個匹配器的情況,那么就需要elem符合全部匹配器規(guī)則
- function(elem, context, xml) {
- var i = matchers.length;
- //從右到左開始匹配
- while (i--) {
- //如果有一個沒匹配中,那就說明該節(jié)點elem不符合規(guī)則
- if (!matchers[i](elem, context, xml)) {
- return false;
- }
- }
- return true;
- } :
- //單個匹配器的話就返回自己即可
- matchers[0];
- }
看代碼大概就知道,就是分解這個子匹配器了,返回又一個curry函數(shù),給addCombinator方法
- //addCombinator方法就是為了生成有位置詞素的匹配器。
- function addCombinator(matcher, combinator, base) {
- var dir = combinator.dir,
- checkNonElements = base && dir === "parentNode",
- donedoneName = done++; //第幾個關(guān)系選擇器
- return combinator.first ?
- // Check against closest ancestor/preceding element
- // 檢查最靠近的祖先元素
- // 如果是緊密關(guān)系的位置詞素
- function(elem, context, xml) {
- while ((elemelem = elem[dir])) {
- if (elem.nodeType === 1 || checkNonElements) {
- //找到***個親密的節(jié)點,立馬就用***匹配器判斷這個節(jié)點是否符合前面的規(guī)則
- return matcher(elem, context, xml);
- }
- }
- } :
- // Check against all ancestor/preceding elements
- //檢查最靠近的祖先元素或兄弟元素(概據(jù)>、~、+還有空格檢查)
- //如果是不緊密關(guān)系的位置詞素
- 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節(jié)點上設(shè)置任意數(shù)據(jù),所以它們不會從dir緩存中受益
- if (xml) {
- while ((elemelem = elem[dir])) {
- if (elem.nodeType === 1 || checkNonElements) {
- if (matcher(elem, context, xml)) {
- return true;
- }
- }
- }
- } else {
- while ((elemelem = elem[dir])) {
- //如果是不緊密的位置關(guān)系
- //那么一直匹配到true為止
- //例如祖宗關(guān)系的話,就一直找父親節(jié)點直到有一個祖先節(jié)點符合規(guī)則為止
- if (elem.nodeType === 1 || checkNonElements) {
- outerCache = elem[expando] || (elem[expando] = {});
- //如果有緩存且符合下列條件則不用再次調(diào)用matcher函數(shù)
- 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為位置詞素
根據(jù)關(guān)系選擇器檢查
如果是這類沒有位置詞素的選擇器:’#id.aaron[name="checkbox"]‘
從右到左依次看看當前節(jié)點elem是否匹配規(guī)則即可。但是由于有了位置詞素,
那么判斷的時候就不是簡單判斷當前節(jié)點了,
可能需要判斷elem的兄弟或者父親節(jié)點是否依次符合規(guī)則。
這是一個遞歸深搜的過程。
所以matchers又經(jīng)過一層包裝了
然后用同樣的方式遞歸下去,直接到tokens分解完畢
返回的結(jié)果一個根據(jù)關(guān)系選擇器分組后在組合的嵌套很深的閉包函數(shù)了
看看結(jié)構(gòu)
但是組合之后怎么執(zhí)行?
superMatcher方法是matcherFromGroupMatchers( elementMatchers, setMatchers )方法return出來的,但是***執(zhí)行起重要作用的是它
下章在繼續(xù),這章主要只是要說說這個編譯函數(shù)的流程,具體還有細節(jié),就需要仔細看代碼,我不能一條一條去分解的,還有函數(shù)具體的用處,就需要結(jié)合后面的才能比較好的理解!
原文鏈接:http://www.cnblogs.com/aaronjs/p/3322466.html
特此感謝Aaron。
【編輯推薦】