资讯专栏INFORMATION COLUMN

Zepto 源码分析 1 - 进入 Zepto

Aklman / 1813人阅读

摘要:选择的理由是一个用于现代浏览器的与大体兼容的库。环境搭建分析环境的搭建仅需要一个常规页面和原始代码一个常规页面打开的首页即可,在开发人员工具中即可使用原始代码本篇分析的代码参照,进入该代码分支中即可。

选择 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 代码参照 Commit 3172e9,进入该代码分支中即可。

热身部分,先从 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 */
"
  dist = cat(module_files).replace(/^/[/*].*$/mg, "").replace(/
{3,}/g, "

")
  
  // 判断是否将 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 进行代码压缩的过程等,无论用 gruntwebpack 组织流水线也只是相似的工序,提供多个出口。

对于 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/$ 这三个组件之间的关系上,处理他们的关系也正是本篇的目的所在。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108672.html

相关文章

  • Zepto 源码分析 4 - 核心模块入口

    摘要:对象构造函数读入的两个参数与在中也有明确的规范,用以保证构造函数的简单性。 承接第三篇末尾内容,本篇结合官方 API 进入对 Zepto 核心的分析,开始难度会比较大,需要重点理解几个核心对象的关系,方能找到线索。 $() 与 Z 对象创建 Zepto Core API 的首个方法 $() 按照其官方解释: Create a Zepto collection object by pe...

    xzavier 评论0 收藏0
  • Zepto 源码分析 3 - qsa 实现与工具函数设计

    摘要:承接第一篇末尾内容,本部分开始进入主模块,分析其设计思路与实现技巧下文代码均进行过重格式化,但代码版本同第一部分内容且入口函数不变的选择器先从第一个与原型链构造不直接相关的工具函数说起,观察的设计思路。 承接第一篇末尾内容,本部分开始进入 zepto 主模块,分析其设计思路与实现技巧(下文代码均进行过重格式化,但代码 Commit 版本同第一部分内容且入口函数不变): Zepto 的选...

    ctriptech 评论0 收藏0
  • Zepto源码之Touch模块

    摘要:在触发事件前,先将保存定时器的变量释放,如果对象中存在,则触发事件,保存的是最后触摸的时间。如果有触发的定时器,清除定时器即可阻止事件的触发。其实就是清除所有相关的定时器,最后将对象设置为。进入时,立刻清除定时器的执行。 大家都知道,因为历史原因,移动端上的点击事件会有 300ms 左右的延迟,Zepto 的 touch 模块解决的就是移动端点击延迟的问题,同时也提供了滑动的 swip...

    Prasanta 评论0 收藏0
  • Zepto 源码分析 2 - Polyfill 设计

    摘要:此模块包含的设计思路即为预以匹配降级方案。没有默认编译该模块,以及利用该模块判断后提供平台相关逻辑的主要原因在于其设计原则的代码完成核心的功能。此处,也引出了代码实现的另一个基本原则面向功能标准,先功能覆盖再优雅降级。 在进入 Zepto Core 模块代码之前,本节简略列举 Zepto 及其他开源库中一些 Polyfill 的设计思路与实现技巧。 涉及模块:IE/IOS 3/Dete...

    chuyao 评论0 收藏0
  • Zepto 源码之神奇的 $

    摘要:返回值为,如果能查找到元素,则将元素以数组的形式返回,否则返回空数组排除不合法的。的第一个字符为,并且为标签。如果存在,则查找下选择器为的所有子元素。正则表达式为如果没有指定标签名,则获取标签名。包裹元素的即为所需要获取的。 经过前面三章的铺垫,这篇终于写到了戏肉。在用 zepto 时,肯定离不开这个神奇的 $ 符号,这篇文章将会看看 zepto 是如何实现 $ 的。 读Zepto源码...

    xi4oh4o 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<