选择 Zepto 的理由
Zepto is a minimalist JavaScript library for modern browsers with a largely jQuery-compatible API. If you use jQuery, you already know how to use Zepto.
Zepto 是一个用于现代浏览器的与 jQuery 大体兼容的 JavaScript 库。如果你使用过 jQuery 的话,你已经知道如何使用 Zepto 了。
Zepto.js 官方网对其进行的描述简而言之即为一个大体兼容 jQuery 的 JavaScript 库,因此是否选择分析 Zepto 这样的库前置问题即为,对于此情此景下的前端入门者(2017 年的前端入行者,Like Me)是否还有分析 jQuery 源码的必要?
我心中的答案,便是有也没有,先说没有必要的一方面:
- jQuery 包含大量针对“两套标准”的兼容实现,最明显的例子即是对 XHR 的处理方式上,因此对于类似的片段,寻找解决方案已经没有太大的意义:
var xhrSuccessStatus = {
// File protocol always yields status code 0, assume 200
0: 200,
// Support: IE <=9 only
// #1450: sometimes IE returns 1223 when it should be 204
1223: 204
},
- jQuery 处理逻辑的基本对象基于 DOM,因此同时包含基础库 Sizzle.js 用以实现纯粹的跨平台选择器实现,但由于 Document.querySelectorAll() 的兼容实现已经覆盖所有现代浏览器,类似这样的判断也没有了太多的借鉴价值:
if ( support.qsa &&
!nonnativeSelectorCache[ selector + " " ] &&
(!rbuggyQSA || !rbuggyQSA.test( selector )) &&
// Support: IE 8 only
// Exclude object elements
(nodeType !== 1 || context.nodeName.toLowerCase() !== "object") )
-再说有必要的一方面,仍然拿 querySelectorAll
举例,Zepto 这一 jQuery 兼容库中将其封装为了如下的 qsa
函数:
zepto.qsa = function(element, selector) {
//\ ...
return element.getElementById && isSimple && maybeID
? (found = element.getElementById(nameOnly))
//\ ...
: slice.call(
isSimple && !maybeID && element.getElementsByClassName
? maybeClass
? element.getElementsByClassName(nameOnly)
: element.getElementsByTagName(selector)
: element.querySelectorAll(selector)
);
};
即在调用该函数时,根据类型先做出判断,如果目标为可识别的类型,那么采取相应的方法进行选择,最终降级到使用 querySelectorAll()
这一函数进行选择。采用该方法的目的即为了尽可能的提高选择器性能,参考 jsperf getElementById vs querySelector 的运行结果便可略知一二:
测试调用 | Ops / Sec | Ops 差距 |
---|---|---|
document.getElementById("foo") | 27,869,670 | fastest |
document.querySelector("#foo") | 11,206,845 | 62% slower |
document.querySelector("[id=foo]") | 11,281,576 | 59% slower |
另一个例子,关于 jQuery 对象本质的问题,以简化的 Zepto 为例,其拥有如下多种的初始化方式:
$()
$(selector, [context]) ⇒ collection
$() ⇒ same collection
$() ⇒ collection
$(htmlString) ⇒ collection
$(htmlString, attributes) ⇒ collection v1.0+
Zepto(function($){ ... })
而其调用生成的对象仅仅形似一个数组,但却如何实现可以简单的操作 DOM 元素:
// 构造一个空的 Zepto(Z) 对象
> $()
[selector: ""]
-> selector: ""
-> length: 0
-> __proto__: Object
// 选择并着色
> $("p:last-child").css('background-color', 'aliceblue')
答案便存在于 Zepto 的模块结构当中,$.fn
包含暴露在外的工具函数,当一个 Z 对象创建时将其设定为原型没,便可获取其中的工具函数:
zepto.Z.prototype = Z.prototype = $.fn;
这样设计思想与实现方式的例子在 jQuery/Zepto 中比比皆是。面对有还是没有必要阅读 jQuery 源码的问题,比起 jQuery 中写满浏览器峥嵘历史的长篇巨著,Zepto 将其简化为了:
- 主体部分 1000 行的代码规模
- 高度的 jQuery API 兼容
- 模块化的组合方式,默认编译方式只包含现代浏览器支持
最大限度的降低了阅读成本,因此该问题的答案,可以回答为去粗取精,通过分析一种 jQuery 20% 代码量的最简核心实现(Zepto),领略其 80% 的设计思想。
环境搭建
Zepto 分析环境的搭建仅需要一个常规页面和原始代码:
热身部分,先从 Zepto 的模块结构开始:
模块结构
Zepto 核心部分包含 zepto module
等五个默认编译入生产代码的模块,并且给出了扩展插件的方法,那么第一个问题便是,Zepto 是如何引入并提供模块结构的:
- 进入
package.json
发现 Zepto 项目提供了三个公用的命令行入口,供coffee-script
使用,实际入口为make
:
"scripts": {
"test": "coffee make test",
"dist": "coffee make dist",
"start": "coffee test/server.coffee"
},
- 进入
make
文件,直接找到它生成dist
的过程:
//\ Line 33
target.dist = ->
target.build()
target.minify()
target.compress()
target.build = ->
cd __dirname
mkdir '-p', 'dist'
//\ 这里标明了 5 个默认的编译模块,可以通过环境变量改变编译目标,且这些模块名称即为对应的文件名
modules = (env['MODULES'] || 'zepto event ajax form ie').split(' ')
module_files = ( "src/#{module}.js" for module in modules )
//\ 声明许可证,放在 dist 头部,将目标源码文件中的注释删除,将 3 行以上的空行转换为两个空行写入
intro = "/* Zepto #{describe_version()} - #{modules.join(' ')} - zeptojs.com/license */\n"
dist = cat(module_files).replace(/^\/[\/*].*$/mg, '').replace(/\n{3,}/g, "\n\n")
//\ 判断是否将 AMD Layout 写入,如果是,则将上文代码填入 AMD 代码段中,回报体积
dist = cat('src/amd_layout.js').replace(/YIELD/, -> dist.trim()) unless env['NOAMD']
(intro + dist).to(zepto_js)
report_size(zepto_js)
几点额外的补充:
-
-
make
中的#!/usr/bin/env coffee
是类 Unix 操作系统中表示文本文件解析器的声明 Shebang),类似于 HTML 文档的DOCTYPE
,该文件写法可明显看出 Linux Shell 脚本的意味,不过采用了shelljs
这一运行在 Nodejs 上的库进行了跨平台。 -
make
中的compress
过程中用到了gzip
进行压缩:inp.pipe(gzip).pipe(out)
,该项压缩对于用户是透明的,用户浏览器可以通过Content-Encoding
HTTP 字段获知该文件已被压缩过并提供预解压操作,详见 MDN - HTTP 协议中的数据压缩。
-
- 该
make
脚本中还有很多的借鉴之处,例如 102 行git
相关操作,以及 108 行开始调用uglify.js
进行代码压缩的过程等,无论用grunt
或webpack
组织流水线也只是相似的工序,提供多个出口。 - 对于
src/amd_layout
这一没被列入模块列表中的文件,其作用即为兼容 AMD 标准:
//\ src/amd_layout
//\ 作用于全局的 IIFE
(function(global, factory) {
//\ AMD 兼容写法
if (typeof define === 'function' && define.amd)
define(function() { return factory(global) })
else
factory(global)
}(window, function(window) {
//\ 如果包含 AMD 编译,就将上文代码完整写入该函数
YIELD
return Zepto
}))
此模块忽略 AMD 相关逻辑会得到一个 JavaScript 库中常见的立即执行表达式(IIFE)结构,使用该结构的目的即为构造块级作用域,防止库中的变量对全局进行污染,是一种非常常用的包装方法,详见 MDN - IIFE:
(function(global, factory) {
//\ ...
}(
//\ ...
))
进入 Zepto 主模块
分析完 Zepto 模块结构,开始进入 Zepto 主模块,主模块框架如下:
//\ src/zepto.js
//\ 将 Zepto 定义为一个 IIFE
var Zepto = (function() {
//\... Zepto 内部变量定义部分 6-47 行
//\... Zepto 内部对象 zepto / 公共函数定义部分 48-167 行
//\... zepto/Z/$ 关系构造
zepto.Z =
zepto.isZ =
zepto.init =
$ =
extend =
$.extend =
//\... 与 DOM 的桥梁 - querySelectorAll 实现
zepto.qsa =
//\ $ 对象对外的工具函数 279-404 行
//\ 定义 $.fn,通过原型链赋予方法
$.fn = {
constructor: zepto.Z,
//\ ...
}
//\ 一些对于 $.fn 的其他函数实现 852-936 行
//\ 继续处理 zepto/Z/$ 的关系,将 Z 的原型指向 $.fn
zepto.Z.prototype = Z.prototype = $.fn
zepto.uniq = uniq
zepto.deserializeValue = deserializeValue
//\ 将内置对象 zepto 挂载到 $ 对象
$.zepto = zepto
//\ 将 $ 作为 IIFE 返回值返回
return $
})()
//\ 将 window.Zepto 指向 Zepto IIFE 的返回值 '$'
window.Zepto = Zepto
//\ 如果 '$' 还未被占用,就将其也初始为 Zepto IIFE 的返回值 '$'
window.$ === undefined && (window.$ = Zepto)
其核心设计思想即体现在 zepto/Z/$
这三个组件之间的关系上,处理他们的关系也正是本篇的目的所在。