Zepto 源码分析 1 - 进入 Zepto
选择 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 $(<Zepto collection>) ⇒ same collection $(<DOM nodes>) ⇒ 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/$
这三个组件之间的关系上,处理他们的关系也正是本篇的目的所在。