介绍

Zepto是一个轻量级的针对现代高级浏览器的JavaScript库, 它与jquery有着类似的api。源码相比jQuery较简单,适合初学者阅读。我们这里从Core模块开始阅读。

Core模块结构

1
2
3
4
5
6
var Zepto = (function() {
...
})()
window.Zepto = Zepto
window.$ === undifined && (window.$ = Zepto)

我们可以看到,Zepto库提供了一个Zepto全局变量,该变量指向了,块级作用域执行后返回的结果,同时将这个全局变量赋值给window对象的Zepto和$变量作为属性。

Core模块核心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var Zepto = (function() {
var undefined, key, $, classList, ...
zepto = {}
...
function Z(dom, selector) {
...
}
...
zepto.Z = function(dom, selector) {
return new Z(dom, selector)
}
...
zepto.init = function(selector, context) {
...
return zepto.Z(dom, selector)
}
...
$ = function(selector, context){
return zepto.init(selector, context)
}
...
zepto.qsa = function(element, selector){
...
}
...
$.fn = {
...
}
zepto.Z.prototype = Z.prototype = $.fn
$.zepto = zepto
return $
})()

Zepto变量指向了模块级作用域返回的$()函数,返回Zepto collection对象,$()函数里调用了zepto局部对象的init()函数,init()函数调用了zepto对象的Z()方法,Z()方法中又创建了Zepto collection对象。同时设置了Z函数显式原型为$.fn对象,并将zepto内部对象设置为$()函数的属性。

Zepto collection对象

1
2
3
4
5
6
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 collection对象是一个类数组对象,它包括了dom对象数组、获取到的dom元素的个数以及选择器。同时该函数对象的原型对象被设置为$.fn。

我个人觉得这个对象较难理解的有以下几点,我会在接下来的博客中解释:

  1. 何为类数组对象。
  2. 原型的设置及隐式原型与显示原型。
  3. 为什么在Chrome console输出Zepto collection对象的时候,对象是被[]数组符号包括的。

逐个方法解析

zepto.init()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
zepto.init = function(selector, context) {
var dom
// 未提供参数,返回空的Zepto collection对象
if (!selector) return zepto.Z()
// 处理参数为字符串的情况
else if (typeof selector == 'string') {
selector = selector.trim()
// 如果是一个html片段,那么根据这个片段创建dom元素
// 如果是以'<'开头,并且通过fragment正则,通过fragment()方法获取dom
// fragment = /^\s*<(\w+|!)[^>]*>/ 匹配空白符等开头,'<>'中间匹配任意字母、下划线或!的匹配项,后面非'>'符号0次或多次
if (selector[0] == '<' && fragmentRE.test(selector))
dom = zepto.fragment(selector, RegExp.$1, context), selector = null
// 有context,根据这个context创建Zepto collection,在该collection上根据selector选择
else if (context !== undefined) return $(context).find(selector)
// CSS选择器
else dom = zepto.qsa(document, selector)
}
// 如果提供了function(),在文档加载时执行function
else if (isFunction(selector)) return $(document).ready(selector)
// 如果Zepto collection已给出,则直接返回
else if (zepto.isZ(selector)) return selector
else {
// 如果选择器是一个数组,将数组中为null的元素去掉
if (isArray(selector)) dom = compact(selector)
// 如果选择器是对象(如dom对象),则将其用数组封装
else if (isObject(selector))
dom = [selector], selector = null
// If it's a html fragment, create nodes from it
else if (fragmentRE.test(selector))
dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null
// If there's a context, create a collection on that context first, and select
// nodes from there
else if (context !== undefined) return $(context).find(selector)
// And last but no least, if it's a CSS selector, use it to select nodes.
else dom = zepto.qsa(document, selector)
}
// create a new Zepto collection from the nodes found
return zepto.Z(dom, selector)
}

上段代码为Zepto选择器功能的逻辑,主要通过以下几种方式进行获取dom:

  • 选择器为字符串
    • 选择器为标签
    • 有context,先获取context,在进行选择
    • 选择器为CSS选择器
  • 选择器为function()
  • 选择器为Zepto collection
  • 选择器为dom、object、数组、html片段等

$.each()

$.each(collection, function(index, item){ … }) => collection
遍历数组元素或以key-value值对方式遍历对象。回调函数返回 false 时停止遍历。

1
2
3
4
5
6
7
8
9
10
11
12
$.each = function(elements, callback){
var i, key
if (likeArray(elements)) {
for (i = 0; i < elements.length; i++)
if (callback.call(elements[i], i, elements[i]) === false) return elements
} else {
for (key in elements)
if (callback.call(elements[key], key, elements[key]) === false) return elements
}
return elements
}

$.each()方法与数组Array.map()方法的区别就是,$.each()方法考虑了集合的情况,使用Array.call()方法对集合内每个元素执行回调函数(i, elements[i]做为参数),并返回处理后结果。

$.contains()

$.contains(parent, node) => boolean
检查父节点是否包含给定的dom节点,如果两者是相同的节点,则返回 false。

1
2
3
4
5
6
7
8
9
$.contains = document.documentElement.contains ?
function(parent, node) {
return parent !== node && parent.contains(node)
} :
function(parent, node) {
while (node && (node = node.parentNode))
if (node === parent) return true
return false
}

方法首先判断了浏览器是否支持Node.contains()方法,如果不支持,则一层一层判断node的父节点和parent是否相等。

$.extend()

$.extend(target, [source, [source2, …]]) => target
$.extend(true, target, [source, …]) => target
通过源对象扩展目标对象的属性,源对象属性将覆盖目标对象属性。
默认情况下为,复制为浅拷贝(浅复制)。如果第一个参数为true表示深度拷贝(深度复制)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function extend(target, source, deep) {
for (key in source)
if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
if (isPlainObject(source[key]) && !isPlainObject(target[key]))
target[key] = {}
if (isArray(source[key]) && !isArray(target[key]))
target[key] = []
extend(target[key], source[key], deep)
}
else if (source[key] !== undefined) target[key] = source[key]
}
$.extend = function(target){
var deep, args = slice.call(arguments, 1)
if (typeof target == 'boolean') {
deep = target
target = args.shift()
}
args.forEach(function(arg){ extend(target, arg, deep) })
return target
}

$.extend方法处理的是一个可选参数的逻辑,首先假设deep为undifined。

  • 如果第一个参数是布尔型则将deep设置为true,将args[0]表示未target, source为args[1…]。
  • 如果第一个参数不是布尔型则deep为undifined,参数target表示target,source为args[0…]。
  • 执行extend()方法,参数为target和source的组合。

extend()方法为一个递归深拷贝的方法,其中处理了当目标target[key]存在或者不存在的情况。

  • 如果存在,则将其覆盖。
  • 如果不存在,则创建新的空对象/数组,进行存储。

$.isNumeric()

$.isNumeric(value) => boolean
如果该值为有限数值或一个字符串表示的数字,则返回ture。

1
2
3
4
5
6
$.isNumeric = function(val) {
var num = Number(val), type = typeof val
return val != null && type != 'boolean' &&
(type != 'string' || val.length) &&
!isNaN(num) && isFinite(num) || false
}

这个方法应该注意的是使用Number(val)进行类型装换后,非字符串类型会被转换为NaN或者Finite类型。

$fn.find()

find(selector) => collection
find(collection) => collection v1.0+
find(element) => collection v1.0+
在当对象前集合内查找符合CSS选择器的每个元素的后代元素。
如果给定Zepto对象集合或者元素,过滤它们,只有当它们在当前Zepto集合对象中时,才回被返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
find: function(selector){
var result, $this = this
if (!selector) result = $()
else if (typeof selector == 'object')
result = $(selector).filter(function(){
var node = this
return emptyArray.some.call($this, function(parent){
return $.contains(parent, node)
})
})
else if (this.length == 1) result = $(zepto.qsa(this[0], selector))
else result = this.map(function(){ return zepto.qsa(this, selector) })
return result
}

$this代表执行find()方法的Zepto collection对象。选择器为collection/dom时,只有当它们在当前Zepto集合对象中时,才回被返回。

$fn.after()

after(content) => self
在每个匹配的元素后插入内容。内容可以为html字符串,dom节点,或者节点组成的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
adjacencyOperators = [ 'after', 'prepend', 'before', 'append' ]
adjacencyOperators.forEach(function(operator, operatorIndex) {
var inside = operatorIndex % 2 // => prepend, append 用于之后判断是否需要选择父节点
$.fn[operator] = function(){
// 参数可以是nodes, arrays of nodes, Zepto objects and HTML strings
var argType, nodes = $.map(arguments, function(arg) {
var arr = []
argType = type(arg)
if (argType == "array") {
arg.forEach(function(el) {
if (el.nodeType !== undefined) return arr.push(el)
else if ($.zepto.isZ(el)) return arr = arr.concat(el.get())
arr = arr.concat(zepto.fragment(el))
})
return arr
}
return argType == "object" || arg == null ?
arg : zepto.fragment(arg)
}),
parent, copyByClone = this.length > 1
if (nodes.length < 1) return this
return this.each(function(_, target){
parent = inside ? target : target.parentNode
// convert all methods to a "before" operation
target = operatorIndex == 0 ? target.nextSibling :
operatorIndex == 1 ? target.firstChild :
operatorIndex == 2 ? target :
null
var parentInDocument = $.contains(document.documentElement, parent)
nodes.forEach(function(node){
if (copyByClone) node = node.cloneNode(true)
else if (!parent) return $(node).remove()
parent.insertBefore(node, target)
if (parentInDocument) traverseNode(node, function(el){
if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
(!el.type || el.type === 'text/javascript') && !el.src){
var target = el.ownerDocument ? el.ownerDocument.defaultView : window
target['eval'].call(target, el.innerHTML)
}
})
})
})
}
}