承接第一篇末尾內容,本部分開始進入 zepto 主模組,分析其設計思路與實現技巧(下文程式碼均進行過重格式化,但程式碼 Commit 版本同第一部分內容且入口函式不變):
Zepto 的選擇器 zepto.qsa()
//\ Line 262
zepto.qsa = function(element, selector) {
};
先從第一個與原型鏈構造不直接相關的工具函式 qsa
說起,觀察 Zepto 的設計思路。
//\ 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 值,用以猜測是否可能為 id
或 class
,此時如果可能是兩者中的一個,那麼去除標記部分(. or #
),否則取自身記為 nameOnly
。simpleSelectorRE
用於測試可能被剝離了一次標記部分的 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
的對映關係將其轉化為對應的字串型別名。
//\ 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 型別的語言屬性。
- 判定元素與選擇器匹配性的函式
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 的增刪改查。