Zepto 源码分析 3 - qsa 实现与工具函数设计
承接第一篇末尾内容,本部分开始进入 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 的增删改查。