Zepto 源碼分析 4 - 核心模塊入口

承接第三篇末尾內容,本篇結合官方 API 進入對 Zepto 核心的分析,開始難度會比較大,須要重點理解幾個核心對象的關係,方能找到線索。css

$() 與 Z 對象建立

Zepto Core API 的首個方法 $() 按照其官方解釋:html

Create a Zepto collection object by performing a CSS selector, wrapping DOM nodes, or creating elements from an HTML string.

經過使用 CSS 選擇器,或包裝 DOM 節點,或從 HTML 片斷中建立一個 Zepto 集合對象。該處即爲核心模塊的入口,對應 src/zepto.js 中的以下調用:node

//\ Line 231
  $ = function(selector, context) {
    return zepto.init(selector, context);
  };
  
  //\ Line 186
  zepto.init = function(selector, context) {
    //\ 該函數的幾種出口
    /*\ 1 */ return zepto.Z();
    /*\ 2 */ return $(context).find(selector);
    /*\ 3 */ return $(document).ready(selector);
    /*\ 4 */ return selector;
    /*\ 5 */ return $(context).find(selector);
    /*\ 6 */ return zepto.Z(dom, selector);
  };

$ 是一個收緊入口的 zepto.init 別名,這樣的函數傳遞使得 zepto.init 的具值參數最多爲 2 個。進入 zepto.init 函數,先忽略中間的處理細節,注意到最終出口共有如上列舉的六種,第 1 種與第 6 種陷入一個名爲 Z 的對象建立過程,下文中發現 $.fn 對象將 constructor 指向了 zepto.Z 且設定了 Z 對象的原型,此處正是 Zepto 的結構組織方式所在:正則表達式

//\ Line 172
  zepto.Z = function(dom, selector) {
    return new Z(dom, selector);
  };

  //\ Line 408
  $.fn = {
    constructor: zepto.Z,
    //\ ...
  }
  
  //\ Line 938
  zepto.Z.prototype = Z.prototype = $.fn;

加載 Zepto 代碼後,可在瀏覽器中進行以下調試便可初步認識 Z 對象的生成:數組

//\ 建立任意一個 Zepto 對象,他的原型均指向 $.fn
> $().__proto__ === $.fn
true

//\ $ 對象中包含了 Zepto 包級別的工具函數,主要用於擴展 Zepto 功能,以 $.func 方式對外暴露
> Object.keys($)
(22) ["extend", "contains", "type", ... ]

//\ $.fn 對象包含了 Zepto 對外暴露的操做 API,面向對象均爲經過 zepto.Z 函數建立的 Z 對象
> Object.keys($.fn)
(75) ["constructor", "length", "forEach", "reduce", ... ]

//\ 所以 Z 對象上的函數調用指向其原型 $.fn 上的同名函數
> $().hasClass === $.fn.hasClass
true

繼續調試過程,分析 Z 對象的實質:瀏覽器

//\ 建立一個空的 Z 對象,發現 Chrome 將其識別爲一個「數組」
> $()
Z [selector: ""]

//\ 但其實際並非 Array 包裝類的一個實例
> $() instanceof Array
false
> $() instanceof Zepto.zepto.Z
true

//\ Zepto 經過 "擴展數組" 這種方式使得其對外體現爲一個對外包含成員變量 selector 的數組,對內能夠進行函數式運算的靈活數據結構
> Object.keys($())
(2) ["length", "selector"]

這樣 "擴展數組" 生成的方法在於這個很是簡明的函數,這裏也揭示了下文中 this 的指向爲一個 Z 對象:數據結構

//\ Line 128
  //\ Z 對象的 Constructor
  function Z(dom, selector) {
    var i,
      len = dom ? dom.length : 0;
    for (i = 0; i < len; i++) this[i] = dom[i];
    this.length = len;
    this.selector = selector || "";
  }

所以,迴歸 zepto.init 方法,其實質即爲調用該構造函數生成 Z 對象(2/3/5 出口返回的是在某個 Z 對象下操做生成的 Z 對象,4 出口實際上傳入的是 Z 對象所以直接返回自身)。Z 對象 構造函數讀入的兩個參數 domselector 在 Zepto 中也有明確的規範,用以保證 Z 構造函數的簡單性。app

dom 的生成與 selector 的取值範圍

dom 的生成嚴格依賴於如下的幾個正則表達式以及其生成函數 zepto.fragment,該步驟比較晦澀,簡而言之功能爲映射原生 dom 結構至相應 Z 對象的關係:dom

//\ Line 10
    //\ 用以匹配普通的 HTML 片斷,這裏面 Group 1 (\w+|!) 用來拿標籤類型,例如: div
    fragmentRE = /^\s*<(\w+|!)[^>]*>/,
    //\ 用以匹配單標籤或兩個閉合標籤間沒有內容的狀況,如:<p> / <abbr></abbr>
    singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/,
    //\ 用以匹配不該自閉合的標籤,Group 1 / Gruop 2 均爲 Group 1 判斷條件約束的非自閉和標籤
    tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,

  //\ Line 140
  zepto.fragment = function(html, name, properties) {
    var dom, nodes, container;

    //\ 若是知足 singleTagRE 就調用 document 建立該元素,再經過 $() 初始化
    if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1));

    if (!dom) {
      //\ 若是聲明 replace 將 html 標籤修復
      if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>");
      //\ name 若是不指定,默認取標籤類型
      if (name === undefined) name = fragmentRE.test(html) && RegExp.$1;
      //\ 若是 name 不在全局變量 containers 內,取 *
      if (!(name in containers)) name = "*";

      //\ 從 containers 拿具體標籤,建立 html 內容
      container = containers[name];
      container.innerHTML = "" + html;
      
      //\ 將 container 中每一個子節點的內容刪除,此時 dom 剩餘部分是一個 Z 元素
      //\ 參考:https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild
      dom = $.each(slice.call(container.childNodes), function() {
        container.removeChild(this);
      });
    }

    //\ 若是傳入第三個參數爲普通對象
    if (isPlainObject(properties)) {
      //\ 根據 dom 建立 Z 對象,再將 properties 中定義的屬性掛載到 $(dom) 上
      nodes = $(dom);
      $.each(properties, function(key, value) {
        if (methodAttributes.indexOf(key) > -1) nodes[key](value);
        else nodes.attr(key, value);
      });
    }

    //\ 返回 dom
    return dom;
  };

分析期間的幾個疑問:函數

  • 爲何有三個正則用於測試:

爲了保證下三種調用方式生成的結果相同(實際上描述的也是同一個空標籤):

> $.zepto.fragment("<div>")
Z [div, selector: ""]
> $.zepto.fragment("<div/>")
Z [div, selector: ""]
> $.zepto.fragment("<div></div>")
Z [div, selector: ""]
  • containercontainers

containers 用於當以 name 爲某個標籤寫爲 innerHTML 時其父元素是合理的,這裏僅在是表格元素的狀況下替換了父容器,其餘狀況下默認採用 div.

//\ Line 22
    containers = {
      tr: document.createElement("tbody"),
      tbody: table,
      thead: table,
      tfoot: table,
      td: tableRow,
      th: tableRow,
      "*": document.createElement("div")
    },
  • isObjectisPlainObject

本函數中斷定第三個參數類型時,採用了 isPlainObject 而不單單是 isObject 是爲了避開數組等其餘可能被識別爲 Object 的狀況,此處校驗該對象的原型。

//\ Line 74
  function isPlainObject(obj) {
    return (
      isObject(obj) &&
      !isWindow(obj) &&
      Object.getPrototypeOf(obj) == Object.prototype
    );
  }

selector 較 DOM 要簡單得多,若是不引入額外的 Selector 模塊,其取值範圍就是 Document.querySelectorAll() 這一瀏覽器原生方法支持的選擇器的取值範圍。(注意 Zepto 源代碼中 selector 多用於標識形參,這裏提到的 selector 其實是 Z 對象中的 selector 屬性)

zepto.init 的六種出口

有了上面的鋪墊,終於能夠進入 zepto.init,解析其六種出口對應的 Z 對象建立或操做過程:

zepto.init = function(selector, context) {
    var dom
    
    //\ 出口 1: 當第一個入參爲 Falsy 值,那麼直接返回一個空的 Z 對象(該出口行爲爲建立)
    //\ 例如: $() / $(undfined)
    if (!selector) return zepto.Z()
    
    //\ 若是 selector 形參是字符串
    else if (typeof selector == 'string') {
      
      //\ 當 selector 形參第一個字符爲 '<' 且符合 fragmentRE 的匹配結果,那麼傳入 zepto.fragment 方法,建立 Z 對象,例如:$("<p></p>")
      //\ 跳至出口 6
      if (selector[0] == '<' && fragmentRE.test(selector))
        dom = zepto.fragment(selector, RegExp.$1, context), selector = null
    
      //\ 出口 2: 若是傳入的 context 不爲空,那麼基於 context 建立一個 Z 對象並返回包含 context 的 Z 對象(該出口行爲爲選擇)
      //\ 例如 $("p", { text:"Hello", id:"greeting", css:{color:'darkblue'} })
      else if (context !== undefined) return $(context).find(selector)
      
      //\ 出口3:若是沒有 context 要求,則直接在全文 qsa(該出口行爲爲選擇)
      //\ 例如 $("p#grt")
      else dom = zepto.qsa(document, selector)
    }
    //\ 出口4:若是 selector 形參爲函數,那將其掛載在 ready 方法內(該出口行爲爲邏輯 Defer)
    //\ 例如 $(() => alert("DONE))
    else if (isFunction(selector)) return $(document).ready(selector)
    
    //\ 出口 5: 當第一個入參已經爲 Z 對象時直接返回自身(該出口行爲爲提供兼容)
    //\ 例如 JSON.stringify($($('p'))) === JSON.stringify($('p')) 
    else if (zepto.isZ(selector)) return selector
    //\ 若是前方判斷條件所有失敗,進入最終的類型判斷
    else {
      //\ 若是傳入的 Selector 是數組,那麼去除 Falsy 值合併,例如 $([$('div'), $('body')])
      if (isArray(selector)) dom = compact(selector)
      //\ 此處爲降級條件,跳入出口 6
      else if (isObject(selector))
        dom = [selector], selector = null
      //\ 剩餘部分參考出口 2/3 處理方式
      else if (fragmentRE.test(selector))
        dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null
      else if (context !== undefined) return $(context).find(selector)
      else dom = zepto.qsa(document, selector)
    }
    
    //\ 出口 6,以上文處理規範的 dom / selector 建立 Z 對象(該出口行爲爲建立)
    return zepto.Z(dom, selector)
  }

此時,也能夠給出 dom 對象與 Z 對象的真正關係,dom 是用於 Z 對象生成過程當中的中間 Z 對象;zepto.initzepto.fragment 必須合併起來才能真正理解 $() 能夠多類型進,單類型出的設計技巧及真實意圖,這也是 Zepto 的精華所在。

相關文章
相關標籤/搜索