Zepto源码学习-核心篇

0.前言

Zepto源码1.2.0未压缩带注释约有1835行,之前是当做设计模式来阅读,并没有深入。且在当前前端环境下,JQuery的重要性大大降低了,从事开发工作大多用的是Angular、Vue等,并没有将jQuery用到精通。以训练为目的,尝试将Zepto源码讲的清楚一点

1.构建Zepto

下载源码并install构建

1
2
3
4
git clone https://github.com/madrobby/zepto.git
cd zepto
npm install
npm run dist

这部分内容可以通过阅读官方仓库指南获得,通过进一步分析package.json文件,可以看出是用coffee-script的cake方法做的构建文件make,但是源码并没有用coffee-script来写,所以不用担心。关于coffee-script,个人觉得如果闲的厉害可以去学学,否则不如去学点别的,或者多看一些源码。

2.模块分析

首先要明白一点,如果第一次看,逐行分析是没有意义的,必须先了解整体结构。先整体再局部,然后细化。
通过阅读手册或者make文件可以知道最终代码会包含zepto event ajax form ie几个模块。实际上,手册排版也大致是按照这些模块划分的。

中文手册

3.整体结构分析

如果对原型链有一定的了解和开发经验,将很容易理解zepto的源码结构,如果是新手,那么先了解原型链,再阅读源码,将会加深对原型链的理解。下面给出打包出的zepto.js整体结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function(global, factory) {
if (typeof define === 'function' && define.amd)
define(function() {
return factory(global)
})
else
factory(global)
}(window, function(window) {
// Zepto核心
var Zepto = (function() {})()
window.Zepto = Zepto
// 一种短路运算,如果成立则执行后面的,否则不执行
window.$ === undefined && (window.$ = Zepto)
// 相应的,也可以这么写
// '$' in window || (window.$ = Zepto);
// 以下是其他Zepto模块
;
(function($) {})(Zepto)
return Zepto
}))

这段代码首先关注最外层的IIFE(立即执行函数表达式),针对AMD模块做了处理。这里可以拓展了解JS的模块化规范CommonJS、AMD等。其他要点已经在代码里做了注释

4.核心模块

上面已经介绍了整个代码的组织结构,接下来分析一下最重要的Zepto核心是怎么实现的。这里可以利用chrome调试工具逐行运行,观察代码的调用栈来了解运行过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script type="text/javascript" src="./dist/zepto.js">

</script>
</head>
<body>
<div class="z"></div>
</body>
<script type="text/javascript">
debugger
$('.z').html('x')
</script>
</html>

写一个简单的页面,从第一行设置断点,然后逐步运行。

4.1 核心模块结构

先看一下简单的结构

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
var Zepto = (function() {
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.Z` swaps out the prototype of the given `dom` array
// of nodes with `$.fn` and thus supplying all the Zepto functions
// to the array. This method can be overridden in plugins.
zepto.Z = function(dom, selector) {
return new Z(dom, selector)
}
zepto.init = function(selector, context) {
// ...
return zepto.Z(dom, selector)
}
// `$` will be the base `Zepto` object. When calling this
// function just call `$.zepto.init, which makes the implementation
// details of selecting nodes and creating Zepto collections
// patchable in plugins.
$ = function(selector, context) {
return zepto.init(selector, context)
}
zepto.Z.prototype = Z.prototype = $.fn
$.zepto = zepto
return $
})()

4.2 zepto.Z和Z对象

这里有一个关键的对象zepto.Z,返回Z对象实例

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 || ''
}

可以打断点各种试一试,其实是将选择器选中的dom数组转为对象

1
console.log('%o',$(document).length)

之所以能访问length是因为Z对象实例本身有这个属性

4.3 zepto.init

为了减轻阅读负担,先简化zepto.init函数。简而言之,zepto.init对selector参数,也就是$(selector,context)传入的第一个参数可能出现的情况编写了逻辑分支。看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
zepto.init = function(selector,context){
var dom
if(!selector){
return zepto.Z()
} else if(typeof selector == 'string'){
// ...
} else if(isFunction(selector)){
return $(document).ready(selector)
} else if(zepto.isZ(selector)){
return selector
} else {
// ...
}
return zepto.Z(dom, selector)
}

首先主要逻辑有五条

  1. selector不存在则返回Z空对象
  2. selector是个string则进一步处理
  3. selector是个function则执行ready
  4. selector是Z实例则原样返回,可多层嵌套$($(selector))验证
  5. else进一步处理

按照顺序本来应该分别阐述各个逻辑的实现细节,不过可以先放一放。目前我们依然不能够去深追细节,而应该继续从结构层面分析后续的内容。目前,我们只需有一个模糊概念:一切似乎都与Z对象有某种联系。

4.4 运行原理

到目前为止,我们分析了$(selector)执行的逻辑从而接触了Z对象。接下去从$(selector).html()执行过程进一步了解zepto核心的解构。

首先我们得思考$(selector)拿到的是一个Z对象实例,那么它为什么能够执行html()方法。

首先我们先看一下Z构造方法

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 || ''
}

实例化后应当只有length,selector这两个属性,以及类似数组下标的dom节点,马上想到其他方法应该是委托给了原型。

1
zepto.Z.prototype = Z.prototype = $.fn

通过查看$.fn源码,果然发现了html()方法的定义,还有许多attr等也同样在这里定义,这样我们至少弄清楚了运行的流程,剩下的按部就班的阅读各个部分的实现细节就可以了。

1
console.log($(document).__proto__===$.fn)//true

最后看看html的实现

1
2
3
4
5
6
7
8
html: function(html){
return 0 in arguments ?
this.each(function(idx){
var originHtml = this.innerHTML
$(this).empty().append( funcArg(this, html, idx, originHtml) )
}) :
(0 in this ? this[0].innerHTML : null)
},

这里面另外又涉及了empty方法,append方法,说明各个方法之间并不是孤立的。建议用debug的方式追溯,这将覆盖绝大部分的源码,所以我们来看看$.fn以外的代码,将$.fn放到最后来看。

4.5 $.extend()方法

查阅手册,$.extend()有两个用法

  • $.extend(target, [source, [source2, …]]) ⇒ target
  • $.extend(true, target, [source, …]) ⇒ target v1.0+

  • 通过源对象扩展目标对象的属性,源对象属性将覆盖目标对象属性。

  • 默认情况下为,复制为浅拷贝(浅复制)。如果第一个参数为true表示深度拷贝(深度复制)。

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
// 观察递归实现拷贝
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]
}
}
}

// Copy all but undefined properties from one or more
// objects to the `target` object.
$.extend = function(target) {

// 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
}

代码应当是很好理解的,顺便可以看看设计的两个工具方法

1
2
3
4
5
6
7
isArray = Array.isArray ||function(object){ return object instanceof Array }
function isPlainObject(obj) {
return isObject(obj) && !isWindow(obj) && Object.getPrototypeOf(obj) == Object.prototype
}

$.isArray = isArray
$.isPlainObject = isPlainObject

我们已经可以参考手册给出的方法顺序,逐个去研究一些工具方法,比如再看一个

  • $.parseJSON
  • $.trim
  • $.noop
    1
    2
    3
    4
    5
    6
    7
    if (window.JSON) $.parseJSON = JSON.parse

    $.trim = function(str) {
    return str == null ? "" : String.prototype.trim.call(str)
    }

    $.noop = function() {}

4.6 其他工具方法

到目前为止,我们已经大致了解Zepto是怎么运行的,随之代码覆盖面的扩大,出现了许多工具库方法,比如isObject,isWindow这些。这个时候可以按照源码顺序,尝试逐行理解代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 驼峰化
camelize = function(str){ return str.replace(/-+(.)?/g, function(match, chr){ return chr ? chr.toUpperCase() : '' }) }
$.camelCase = camelize

$.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
}
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
}
1
2
3
4
// filter是数组原生filter
$.grep = function(elements, callback){
return filter.call(elements, callback)
}

差不多覆盖文档中所有的方法,只剩下–$.fn

4.6 $.fn

参考阅读

Author: sumshare
Link: http://blog.sumshare.cn/2019/04/28/zepto/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.