jQuery 2.0.3 源碼分析Sizzle引擎 - 編譯函數(大篇幅)

jQuery 2.0.3 源碼分析Sizzle引擎 - 編譯函數(大篇幅)

聲明:本文爲原創文章,如需轉載,請註明來源並保留原文鏈接Aaron,謝謝!

從Sizzle1.8開始,這是Sizzle的分界線了,引入了編譯函數機制

網上基本沒有資料細說這個東東的,sizzle引入這個實現主要的作用是分詞的篩選,提高逐個匹配的效率

我們不直接看代碼的實現,通過簡單的實現描述下原理:

以下是個人的理解,如果有錯誤歡迎指出!

 


Javascript有預編譯與我們說的編譯函數是不同的概念

什麼是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個平臺的應用)

5M32C932CWKMGQH_WC_thumb1 6A`{YX`%}M)XF0OMPE4T(JV_thumb[2]

 

通過這個PPT直接描述出用戶行爲的數據,然後直接打包生成相對應的實現應用了,實現部分是JS+CSS3+html5 ,關鍵是可以跨平臺哦

PC上的效果

頁面的元素都是動態的可運行可以交互的

image_thumb7 

移動端的效果

image_thumb10

編譯出來的的APK

image_thumb8

通過一套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

image

每一種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

   1:      //編譯函數機制
   2:      //通過傳遞進來的selector和match生成匹配器:
   3:      compile = Sizzle.compile = function(selector, group /* Internal Use Only */ ) {
   4:          var i,
   5:              setMatchers = [],
   6:              elementMatchers = [],
   7:              cached = compilerCache[selector + " "];
   8:          if (!cached) { //依舊看看有沒有緩存
   9:              // Generate a function of recursive functions that can be used to check each element
  10:              if (!group) {
  11:                  //如果沒有詞法解析過
  12:                  group = tokenize(selector);
  13:              }
  14:              i = group.length; //從後開始生成匹配器
  15:              //如果是有並聯選擇器這裏多次等循環
  16:              while (i--) {
  17:                  //這裏用matcherFromTokens來生成對應Token的匹配器
  18:                  cached = matcherFromTokens(group[i]);
  19:                  if (cached[expando]) {
  20:                      setMatchers.push(cached);
  21:                  } else { //普通的那些匹配器都壓入了elementMatchers裏邊
  22:                      elementMatchers.push(cached);
  23:                  }
  24:              }
  25:              // Cache the compiled function
  26:              // 這裏可以看到,是通過matcherFromGroupMatchers這個函數來生成最終的匹配器
  27:              cached = compilerCache(selector, matcherFromGroupMatchers(elementMatchers, setMatchers));
  28:          }
  29:          //把這個終極匹配器返回到select函數中
  30:          return cached;
  31:      };

  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((tokens = 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",
            doneName = done++; //第幾個關係選擇器

        return combinator.first ?
        // Check against closest ancestor/preceding element
        // 檢查最靠近的祖先元素
        // 如果是緊密關係的位置詞素
        function(elem, context, xml) {
            while ((elem = 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 ((elem = elem[dir])) {
                    if (elem.nodeType === 1 || checkNonElements) {
                        if (matcher(elem, context, xml)) {
                            return true;
                        }
                    }
                }
            } else {
                while ((elem = 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分解完畢

返回的結果一個根據關係選擇器分組後在組合的嵌套很深的閉包函數了

看看結構

image

 


但是組合之後怎麼執行?

superMatcher方法是matcherFromGroupMatchers( elementMatchers, setMatchers )方法return出來的,但是最後執行起重要作用的是它

下章在繼續,這章主要只是要說說這個編譯函數的流程,具體還有細節,就需要仔細看代碼,我不能一條一條去分解的,還有函數具體的用處,就需要結合後面的才能比較好的理解!

哥們,別光看不頂啊!


發佈了16 篇原創文章 · 獲贊 8 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章