Zepto 源碼分析 3 - qsa 實現與工具函數設計

承接第一篇末尾內容,本部分開始進入 zepto 主模塊,分析其設計思路與實現技巧(下文代碼均進行太重格式化,但代碼 Commit 版本同第一部份內容且入口函數不變):node

Zepto 的選擇器 zepto.qsa()

//\ Line 262
  zepto.qsa = function(element, selector) {
  };

先從第一個與原型鏈構造不直接相關的工具函數 qsa 提及,觀察 Zepto 的設計思路。web

//\ Line 28
  simpleSelectorRE = /^[\w-]*$/,

    //\ Line 337
    var found,
      maybeID = selector[0] == "#",
      maybeClass = !maybeID && selector[0] == ".",
      nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked
      isSimple = simpleSelectorRE.test(nameOnly);

函數開始部分先定義了幾個 Bool 值,用以猜想是否可能爲 idclass,此時若是多是二者中的一個,那麼去除標記部分(. or #),不然取自身記爲 nameOnlysimpleSelectorRE 用於測試可能被剝離了一次標記部分的 selector 是否知足是通常字符串的要求,若是不是,那麼可能查詢目標是多個條件組合(如 .class1.class2),後面直接放入原生的 querySelectorAll 方法查詢。數組

//\ Line 268
   return element.getElementById && isSimple && maybeID // Safari DocumentFragment doesn't have getElementById
      ? (found = element.getElementById(nameOnly))
        ? [found]
        : []

進入包含一系列判斷的 return 階段,268 行中出現了一個兼容性註釋,因爲前方的 maybeClass 定義中聲明瞭並不是 id 因此此處不支持 getElementById 方法也將直接陷入原生的 querySelectorAll 方法。若是知足查詢條件則發給原生 getElementById` 方法查詢,返回數組方式的結果。瀏覽器

//\ Line 6
    var undefined,
    key,
    $,
    classList,
    emptyArray = [],
    concat = emptyArray.concat,
    filter = emptyArray.filter,
    slice = emptyArray.slice,

      //\ Line 270
      : element.nodeType !== 1 &&
        element.nodeType !== 9 &&
        element.nodeType !== 11
      ? []
      : slice.call(
          isSimple && !maybeID && element.getElementsByClassName // DocumentFragment doesn't have getElementsByClassName/TagName
            ? maybeClass
              ? element.getElementsByClassName(nameOnly) // If it's simple, it could be a class
              : element.getElementsByTagName(selector) // Or a tag
            : element.querySelectorAll(selector) // Or it's not simple, and we need to query all
        );

先參照 nodeType 判斷了根搜索元素類型,此處採用了和 id 相同的降級策略,並經過調用空數組上方法的方式調用了 Array.prototype 上的 slice 方法完成數組生成,總體 Zepto 庫實際上使用了相同的思想利用原型鏈給予 Z 對象上的操做方法。緩存

Zepto 的幾個工具函數設計

Zepto 的數組與對象相關工具函數較類似於 Underscore.js 先行略去,着重列舉幾個有技巧的實現:數據結構

  • 類型相關工具函數的例子:
//\ Line 29
  class2type = {},
  toString = class2type.toString,

  //\ Line 401
  // Populate the class2type map
  $.each(
    "Boolean Number String Function Array Date RegExp Object Error".split(" "),
    function(i, name) {
      class2type["[object " + name + "]"] = name.toLowerCase();
    }
  );

  //\ Line 65
  function type(obj) {
    return obj == null ? String(obj) :
      class2type[toString.call(obj)] || "object"
  }

工具函數 type 中出現了 == 運算符,此處利用了 null/undefined == null 的語言特性,並經過 String 包裝類進行類型轉換獲得其類型的字符串表示,若是並不是爲這兩種類型,則經過 class2type 的映射關係將其轉化爲對應的字符串類型名。app

//\ Line 78
  function likeArray(obj) {
    var length = !!obj && 'length' in obj && obj.length,
      type = $.type(obj)

    return 'function' != type && !isWindow(obj) && (
      'array' == type || length === 0 ||
        (typeof length == 'number' && length > 0 && (length - 1) in obj)
    )
  }

工具函數 likeArray 實際上給出了 Zepto 所認爲的數組形式,即:存在正 length 的 Number 型成員變量及 Key 值爲 length - 1 的成員變量且並不是是函數的對象。這樣定義可使得迭代器模式可使用,且剛好使用了未初始化的數組項爲 undefined 類型的語言屬性。dom

  • 斷定元素與選擇器匹配性的函數 matches

qsa() 函數相似,Zepto 還給出了一個類型匹配函數 zepto.matches() 用於判斷某個元素是否與一個給定的選擇器匹配:函數

//\ Line 33
tempParent = document.createElement('div'),

  //\ Line 51
  zepto.matches = function(element, selector) {
  
    //\ 若是不知足匹配的類型條件,那麼返回結果爲 False
    if (!selector || !element || element.nodeType !== 1) return false;
    
    //\ Element.prototype.matches() - 斷定某個元素是否符合某個選擇器
    //\ https://dom.spec.whatwg.org/#dom-element-matches
    var matchesSelector =
      element.matches ||
      element.webkitMatchesSelector ||
      element.mozMatchesSelector ||
      element.oMatchesSelector ||
      element.matchesSelector;
    if (matchesSelector) return matchesSelector.call(element, selector);
    
    //\ 若是當前瀏覽器未實現 matches API,則降級爲使用 qsa 函數完成
    //\ 若是父節點存在,則選取父節點進行 qsa()
    //\ 若是父節點不存在,將目標節點放入預約的父節點中,再在父節點上進行 qsa() 檢驗是否能夠找到子節點
    // fall back to performing a selector:
    var match,
      parent = element.parentNode,
      temp = !parent;
    if (temp) (parent = tempParent).appendChild(element);
    match = ~zepto.qsa(parent, selector).indexOf(element);
    
    //\ 清除可能建立的父節點
    temp && tempParent.removeChild(element);
    return match;
  };

類似的構造父級容器以查詢子級元素性質思路在 Zepto 源代碼中屢次出現,例如對於另外一個工具函數 defaultDisplay 的實現中。工具

  • 獲取當前瀏覽器下某元素默認 display 值的 defaultDisplay() 函數,因爲 DOM 中的元素默認樣式值實際上在用戶進行更改前即爲瀏覽器賦予節點類型的默認值,所以查詢元素的默認值能夠變爲查詢某節點類型的默認值:
//\ Line 8
elementDisplay = {}

  //\ Line 109
  function defaultDisplay(nodeName) {
    var element, display;
    //\ 若是全局 elementDisplay 對象中已經緩存了查詢目標 nodeName 的結果那麼直接查詢,不然陷入邏輯
    if (!elementDisplay[nodeName]) {
      
      //\ 建立一個同類型節點,將其放入 body 下獲取它的實時計算值中的 display 屬性
      element = document.createElement(nodeName);
      document.body.appendChild(element);
      
      //\ 此處引用了 IE 模塊中的 getComputedStyle() 函數降級
      display = getComputedStyle(element, "").getPropertyValue("display");
      
      //\ 刪除用於取值的元素對象,若是元素的 display 值爲 none 那麼將其值設爲 block
      //\ 此處將 none 置爲 display 的緣由爲 $.fn.show() 函數中經過該函數獲取一個非隱藏型的默認值
      element.parentNode.removeChild(element);
      display == "none" && (display = "block");
      
      //\ 緩存結果值至全局變量 elementDisplay
      elementDisplay[nodeName] = display;
    }
    return elementDisplay[nodeName];
  }
  
    //\ Line 574
    show: function() {
      return this.each(function() {
        this.style.display == "none" && (this.style.display = "");
        
        //\ defaultDisplay() 獲取值爲 none 時設定爲 block 的緣由
        if (getComputedStyle(this, "").getPropertyValue("display") == "none")
          this.style.display = defaultDisplay(this.nodeName);
      });
     },

Zepto 加載擴展的方法

本節末尾,簡單介紹一下擴展 Zepto 的方法。在主模塊 Zepto 外,一個未默認編譯的模塊 Selector 包含了擴展原 qsa() 函數的實現,進入模塊代碼 src/selector.js,其結構以下:

(function($) {
  var zepto = $.zepto,
    oldQsa = zepto.qsa,
    oldMatches = zepto.matches;

  zepto.qsa = function(node, selector) {
      //\ 擴展的 zepto.qsa 實現
  };

  zepto.matches = function(node, selector) {
      //\ 擴展的 zepto.matches 實現
  };
})(Zepto);

在實際編譯中只需將 Selector 在覈心模塊後編譯便可替換原始的 qsa 函數與對應的 matches 函數,所以基於該思路的 Zepto 外掛模塊很是簡單。在分析核心模塊邏輯時,能夠經過此方法改寫函數,或者嘗試基於業務需求配置一個新的數據結構,再利用 Zepto 實現對 DOM 的增刪改查。

相關文章
相關標籤/搜索