1. 程式人生 > >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出來的,但是最後執行起重要作用的是它

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

哥們,別光看不頂啊!