资讯专栏INFORMATION COLUMN

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

xzavier / 3201人阅读

摘要:对象构造函数读入的两个参数与在中也有明确的规范,用以保证构造函数的简单性。

承接第三篇末尾内容,本篇结合官方 API 进入对 Zepto 核心的分析,开始难度会比较大,需要重点理解几个核心对象的关系,方能找到线索。

$() 与 Z 对象创建

Zepto Core API 的首个方法 $() 按照其官方解释:

Create a Zepto collection object by performing a CSS selector, wrapping DOM nodes, or creating elements from an HTML string.

通过使用 CSS 选择器,或包装 DOM 节点,或从 HTML 片段中创建一个 Zepto 集合对象。该处即为核心模块的入口,对应 src/zepto.js 中的如下调用:

  // Line 231
  $ = function(selector, context) {
    return zepto.init(selector, context);
  };
  
  // Line 186
  zepto.init = function(selector, context) {
    // 该函数的几种出口
    /* 1 */ return zepto.Z();
    /* 2 */ return $(context).find(selector);
    /* 3 */ return $(document).ready(selector);
    /* 4 */ return selector;
    /* 5 */ return $(context).find(selector);
    /* 6 */ return zepto.Z(dom, selector);
  };

$ 是一个收紧入口的 zepto.init 别名,这样的函数传递使得 zepto.init 的具值参数最多为 2 个。进入 zepto.init 函数,先忽略中间的处理细节,注意到最终出口共有如上列举的六种,第 1 种与第 6 种陷入一个名为 Z 的对象创建过程,下文中发现 $.fn 对象将 constructor 指向了 zepto.Z 且设定了 Z 对象的原型,此处正是 Zepto 的结构组织方式所在:

  // Line 172
  zepto.Z = function(dom, selector) {
    return new Z(dom, selector);
  };

  // Line 408
  $.fn = {
    constructor: zepto.Z,
    // ...
  }
  
  // Line 938
  zepto.Z.prototype = Z.prototype = $.fn;

加载 Zepto 代码后,可在浏览器中进行如下调试即可初步认识 Z 对象的生成:

// 创建任意一个 Zepto 对象,他的原型均指向 $.fn
> $().__proto__ === $.fn
true

// $ 对象中包含了 Zepto 包级别的工具函数,主要用于扩展 Zepto 功能,以 $.func 方式对外暴露
> Object.keys($)
(22) ["extend", "contains", "type", ... ]

// $.fn 对象包含了 Zepto 对外暴露的操作 API,面向对象均为通过 zepto.Z 函数创建的 Z 对象
> Object.keys($.fn)
(75) ["constructor", "length", "forEach", "reduce", ... ]

// 因此 Z 对象上的函数调用指向其原型 $.fn 上的同名函数
> $().hasClass === $.fn.hasClass
true

继续调试过程,分析 Z 对象的实质:

// 创建一个空的 Z 对象,发现 Chrome 将其识别为一个“数组”
> $()
Z [selector: ""]

// 但其实际并不是 Array 包装类的一个实例
> $() instanceof Array
false
> $() instanceof Zepto.zepto.Z
true

// Zepto 通过 "扩展数组" 这种方式使得其对外体现为一个对外包含成员变量 selector 的数组,对内可以进行函数式运算的灵活数据结构
> Object.keys($())
(2) ["length", "selector"]

这样 "扩展数组" 生成的方法在于这个非常简明的函数,这里也揭示了下文中 this 的指向为一个 Z 对象:

  // Line 128
  // Z 对象的 Constructor
  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.init 方法,其实质即为调用该构造函数生成 Z 对象(2/3/5 出口返回的是在某个 Z 对象下操作生成的 Z 对象,4 出口实际上传入的是 Z 对象因此直接返回自身)。Z 对象 构造函数读入的两个参数 domselector 在 Zepto 中也有明确的规范,用以保证 Z 构造函数的简单性。

dom 的生成与 selector 的取值范围

dom 的生成严格依赖于以下的几个正则表达式以及其生成函数 zepto.fragment,该步骤比较晦涩,简而言之功能为映射原生 dom 结构至相应 Z 对象的关系:

    // Line 10
    // 用以匹配普通的 HTML 片段,这里面 Group 1 (w+|!) 用来拿标签类型,例如: div
    fragmentRE = /^s*<(w+|!)[^>]*>/,
    // 用以匹配单标签或两个闭合标签间没有内容的情况,如:

/ singleTagRE = /^<(w+)s*/?>(?:|)$/, // 用以匹配不应自闭合的标签,Group 1 / Gruop 2 均为 Group 1 判断条件约束的非自闭和标签 tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([w:]+)[^>]*)/>/gi, // Line 140 zepto.fragment = function(html, name, properties) { var dom, nodes, container; // 如果满足 singleTagRE 就调用 document 创建该元素,再通过 $() 初始化 if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1)); if (!dom) { // 如果声明 replace 将 html 标签修复 if (html.replace) html = html.replace(tagExpanderRE, "<$1>"); // name 如果不指定,默认取标签类型 if (name === undefined) name = fragmentRE.test(html) && RegExp.$1; // 如果 name 不在全局变量 containers 内,取 * if (!(name in containers)) name = "*"; // 从 containers 拿具体标签,创建 html 内容 container = containers[name]; container.innerHTML = "" + html; // 将 container 中每个子节点的内容删除,此时 dom 剩余部分是一个 Z 元素 // 参考:https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild dom = $.each(slice.call(container.childNodes), function() { container.removeChild(this); }); } // 如果传入第三个参数为普通对象 if (isPlainObject(properties)) { // 根据 dom 创建 Z 对象,再将 properties 中定义的属性挂载到 $(dom) 上 nodes = $(dom); $.each(properties, function(key, value) { if (methodAttributes.indexOf(key) > -1) nodes[key](value); else nodes.attr(key, value); }); } // 返回 dom return dom; };

分析期间的几个疑问:

为什么有三个正则用于测试:

为了保证下三种调用方式生成的结果相同(实际上描述的也是同一个空标签):

> $.zepto.fragment("
") Z [div, selector: ""] > $.zepto.fragment("
") Z [div, selector: ""] > $.zepto.fragment("
") Z [div, selector: ""]

containercontainers

containers 用于当以 name 为某个标签写为 innerHTML 时其父元素是合理的,这里仅在是表格元素的情况下替换了父容器,其他情况下默认采用 div.

    // Line 22
    containers = {
      tr: document.createElement("tbody"),
      tbody: table,
      thead: table,
      tfoot: table,
      td: tableRow,
      th: tableRow,
      "*": document.createElement("div")
    },

isObjectisPlainObject

本函数中判定第三个参数类型时,采用了 isPlainObject 而不仅仅是 isObject 是为了避开数组等其他可能被识别为 Object 的情况,此处校验该对象的原型。

  // Line 74
  function isPlainObject(obj) {
    return (
      isObject(obj) &&
      !isWindow(obj) &&
      Object.getPrototypeOf(obj) == Object.prototype
    );
  }

selector 较 DOM 要简单得多,如果不引入额外的 Selector 模块,其取值范围就是 Document.querySelectorAll() 这一浏览器原生方法支持的选择器的取值范围。(注意 Zepto 源代码中 selector 多用于标识形参,这里提到的 selector 实际上是 Z 对象中的 selector 属性)

zepto.init 的六种出口

有了上面的铺垫,终于可以进入 zepto.init,解析其六种出口对应的 Z 对象创建或操作过程:

  zepto.init = function(selector, context) {
    var dom
    
    // 出口 1: 当第一个入参为 Falsy 值,那么直接返回一个空的 Z 对象(该出口行为为创建)
    // 例如: $() / $(undfined)
    if (!selector) return zepto.Z()
    
    // 如果 selector 形参是字符串
    else if (typeof selector == "string") {
      
      // 当 selector 形参第一个字符为 "<" 且符合 fragmentRE 的匹配结果,那么传入 zepto.fragment 方法,创建 Z 对象,例如:$("

") // 跳至出口 6 if (selector[0] == "<" && fragmentRE.test(selector)) dom = zepto.fragment(selector, RegExp.$1, context), selector = null // 出口 2: 如果传入的 context 不为空,那么基于 context 创建一个 Z 对象并返回包含 context 的 Z 对象(该出口行为为选择) // 例如 $("p", { text:"Hello", id:"greeting", css:{color:"darkblue"} }) else if (context !== undefined) return $(context).find(selector) // 出口3:如果没有 context 要求,则直接在全文 qsa(该出口行为为选择) // 例如 $("p#grt") else dom = zepto.qsa(document, selector) } // 出口4:如果 selector 形参为函数,那将其挂载在 ready 方法内(该出口行为为逻辑 Defer) // 例如 $(() => alert("DONE)) else if (isFunction(selector)) return $(document).ready(selector) // 出口 5: 当第一个入参已经为 Z 对象时直接返回自身(该出口行为为提供兼容) // 例如 JSON.stringify($($("p"))) === JSON.stringify($("p")) else if (zepto.isZ(selector)) return selector // 如果前方判断条件全部失败,进入最终的类型判断 else { // 如果传入的 Selector 是数组,那么去除 Falsy 值合并,例如 $([$("div"), $("body")]) if (isArray(selector)) dom = compact(selector) // 此处为降级条件,跳入出口 6 else if (isObject(selector)) dom = [selector], selector = null // 剩余部分参考出口 2/3 处理方式 else if (fragmentRE.test(selector)) dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null else if (context !== undefined) return $(context).find(selector) else dom = zepto.qsa(document, selector) } // 出口 6,以上文处理规范的 dom / selector 创建 Z 对象(该出口行为为创建) return zepto.Z(dom, selector) }

此时,也可以给出 dom 对象与 Z 对象的真正关系,dom 是用于 Z 对象生成过程中的中间 Z 对象;zepto.initzepto.fragment 必须合并起来才能真正理解 $() 可以多类型进,单类型出的设计技巧及真实意图,这也是 Zepto 的精华所在。

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

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

相关文章

  • Zepto 源码分析 1 - 进入 Zepto

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

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

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

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

    摘要:模块处理的是表单提交。表单提交包含两部分,一部分是格式化表单数据,另一部分是触发事件,提交表单。最终返回的结果是一个数组,每个数组项为包含和属性的对象。否则手动绑定事件,如果没有阻止浏览器的默认事件,则在第一个表单上触发,提交表单。 Form 模块处理的是表单提交。表单提交包含两部分,一部分是格式化表单数据,另一部分是触发 submit 事件,提交表单。 读 Zepto 源码系列文章已...

    陈江龙 评论0 收藏0
  • zepto源码分析-代码结构

    摘要:本来想学习一下的源码,但由于的源码有多行,设计相当复杂,所以决定从开始,分析一个成熟的框架的代码结构及执行步骤。同时发表在我的博客源码分析代码结构 本来想学习一下jQuery的源码,但由于jQuery的源码有10000多行,设计相当复杂,所以决定从zepto开始,分析一个成熟的框架的代码结构及执行步骤。 网上也有很多zepto的源码分析,有的给源码添加注释,有的谈与jQuery的不同,...

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

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

    chuyao 评论0 收藏0

发表评论

0条评论

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