经过前面三章的铺垫,这篇终于写到了戏肉。在用 zepto 时,肯定离不开这个神奇的 $ 符号,这篇文章将会看看 zepto 是如何实现 $ 的。

读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto

源码版本

本文阅读的源码为 zepto1.2.0

zepto的css选择器 zepto.qsa

我们都知道,很多时候,我们都用$ 来获取DOM对象,这跟 zepto.qsa 有很大的关系。

源码

zepto.qsa = function(element, selector) {
var found, // 已经找的到DOM
maybeID = selector[0] == '#', // 是否为ID
maybeClass = !maybeID && selector[0] == '.', // 是否为class
nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // 将id或class前面的符号去掉
isSimple = simpleSelectorRE.test(nameOnly) // 是否为单个选择器
return (element.getElementById && isSimple && maybeID) ?
((found = element.getElementById(nameOnly)) ? [found] : []) :
(element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
slice.call(
isSimple && !maybeID && element.getElementsByClassName ?
maybeClass ? element.getElementsByClassName(nameOnly) :
element.getElementsByTagName(selector) :
element.querySelectorAll(selector)
)
}

以上是 qsa 的所有代码,里面有用到一个正则表达式 simpleSelectorRE,先将这个正则消化下。

simpleSelectorRE = /^[\w-]*$/,

看到这个正则其实是匹配 a-z、A-Z、0-9、下划线、连词符 组合起来的单词,这其实就是单个 idclass 的命名规则。

return 中可以看出,qsa 其实是根据不同情况分别调用了原生的 getElementByIdgetElementsByClassNamegetElementsByTagNamequerySelectorAll 的方法。

为什么要这么麻烦,不直接调用 querySelectorAll 方法呢?这是出于性能的考虑。这里有个简单的测试。这个测试里,页面上只有一个元素,如果比较复杂的时候,差距更加明显。

好了,开始逐行分析代码。

参数

  • element 开始查找的元素
  • selector 选择器

变量

  • found: 已经找到的元素
  • maybeID = selector[0] == '#': 判断选择器的第一个字符是否为 #, 如果是 # ,则可能是 id 选择器
  • maybeClass = !maybeID && selector[0] == '.' 如果不是 id 选择器,并且选择器的第一个字符为 . ,则可能是 class 选择器
  • nameOnly = maybeID || maybeClass ? selector.slice(1) : selector ,如果为 id 选择器或者 class 选择器,则将第一个字符去掉
  • isSimple = simpleSelectorRE.test(nameOnly) 是否为单选择器,即 .single 的形式,不是 .first .secend 等形式

element.getElementById

(element.getElementById && isSimple && maybeID) 这是采用 element.getElementById 的条件。

首先要确保 element 具有 getElementById 的方法。getElementById 的方法是在 document 上的,Chrome等浏览器上,element 可能并不具有 geElementById 的方法,具体可以看看这篇文章:各浏览器对document.getElementById等方法的实现差异解析

然后要确保选择器为单选择器,并且为 id 选择器。

返回值为 ((found = element.getElementById(nameOnly)) ? [found] : []), 如果能查找到元素,则将元素以数组的形式返回,否则返回空数组

排除不合法的element

element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 111 对应的是 Node.ELEMENT_NODE9 对应的是 Node.DOCUMENT_NODE11 对应的是 Node.DOCUMENT_FRAGMENT_NODE ,如果不为以上三种类型,直接返回 []

终极三元表达式

slice.call(
isSimple && !maybeID && element.getElementsByClassName ? // 如果为单选择器并且不为id选择器并且存在getElementsByClassName方法,进入下一个三元表达式判断
maybeClass ? element.getElementsByClassName(nameOnly) : // 如果为class选择器,则采用getElementsByClassName
element.getElementsByTagName(selector) : // 否则采用getElementsByTagName方法
element.querySelectorAll(selector) // 以上情况都不是,则用querySelectorAll
)

这里用了 slice.call 处理所获取到的集合,这样,获取到的DOM集合就可以直接使用数组的方法了。

zepto.Z 函数

从第一篇代码结构中我们已经知道,其实实现 $ 函数的核心是 zepto.init ,而 zepto.init 最终返回的是 zepto.Z 的结果。那就先来看看 zepto.Z

zepto.Z = function(dom, selector) {
return new Z(dom, selector)
}

zepto.Z 的代码很简单,返回的是 Z 函数的实例。那接下来再看看 Z 函数:

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

Z 函数做的事情也很简单,就是将 dom 数组转化为类数组的形式,并设置对应的 length 属性和 selector 属性。

zepto.isZ

zepto.isZ = function(object) {
return object instanceof zepto.Z
}

既然看了 Z 函数,就顺便也将 isZ 也一起看了吧。isZ 函数用来判断参数 object 是否为 Z 的实例,这在 init 中会用到。

$的实现 zepto.init 函数

$的实现

$ = function(selector, context) {
return zepto.init(selector, context)
}

可以看到,其实 $ 调用的就是 zepto.init 这个内部方法。

zepto.init

zepto.init = function(selector, context) {
var dom // dom 集合
if (!selector) return zepto.Z() // 分支1
else if (typeof selector == 'string') { // 分支2
selector = selector.trim()
if (selector[0] == '<' && fragmentRE.test(selector))
dom = zepto.fragment(selector, RegExp.$1, context), selector = null
else if (context !== undefined) return $(context).find(selector)
else dom = zepto.qsa(document, selector)
}
else if (isFunction(selector)) return $(document).ready(selector) // 分支3
else if (zepto.isZ(selector)) return selector // 分支4
else { // 分支5
if (isArray(selector)) dom = compact(selector)
else if (isObject(selector))
dom = [selector], selector = null
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)
}
return zepto.Z(dom, selector)
}

这个 init 方法代码量不多,但是有大量的 if else, 希望我可以说得清楚

$的用法

$(selector, [context])   ⇒ collection  // 用法1
$(<Zepto collection>) ⇒ same collection // 用法2
$(<DOM nodes>) ⇒ collection // 用法3
$(htmlString) ⇒ collection // 用法4
$(htmlString, attributes) ⇒ collection v1.0+ // 用法5
Zepto(function($){ ... }) // 用法6

不传参调用

直接调用 $() 时,对应的是分支1的情况: if (!selector) return zepto.Z() ,返回的是空的 Z 对象

selectorString

selectorstring 时,对应的代码在分支2,对应的用法是用法1用法4用法5

在这个分支里,又有三个子分支。一一来看一下:

第一个的判断条件为 selector[0] == '<' && fragmentRE.test(selector)selector 的第一个字符为 < ,并且为html标签 。fragmentRE 的定义如下 fragmentRE = /^\s*<(\w+|!)[^>]*>/ ,这个其实就是用来判断字符串是否为标签。 我对正则也不太熟,这里就不再展开。

如果满足条件,则执行如下代码:dom = zepto.fragment(selector, RegExp.$1, context), selector = nullzepto.fragment 其实是通过 htmlString 返回一个dom集合。这个函数稍后会说到,这里先不展开。这里对应的是用法4用法5

如果不满足第一个判断条件,则再判断 context !== undefined (上下文是否存在)。如果存在,则查找 context 下选择器为 selector 的所有子元素: $(context).find(selector) 。这个分支对应的是用法1

否则,调用 zepto.qsa 方法,查找 document 下的所有 selectordom = zepto.qsa(document, selector)。这里对应的是用法1

selectorFunction

对应的代码在分支3,对应的用法是用法6

这个分支很简单,在页面加载完毕后,再执行回调方法:$(document).ready(selector)

用过 zepto 的应该都熟悉这种用法: $(function() {})。其实走的就是这个分支

selectorZ 对象时

对应的代码在分支4,对应的用法是用法2

如果参数已经为 Z 对象(zepto.isZ(selector)),则不需要做任何事情,直接原对象返回就可以了。

selector 为其他情况

如果为数组时(isArray(selector)), 将数组展平(dom = compact(selector))

如果为对象时(isObject(selector)),将对象包裹成数组(dom = [selector])。

以上两种情况对应的是用法3,将dom对象或dom集合转化为 z 对象

如果为标签(fragmentRE.test(selector)),执行跟分支1一模一样的代码。这里判断在上面已经做过了,为什么要再来一次呢?我也不太明白,有明白的可以跟我说下。

经过一轮又一轮的判断和 selector 重置,现在终于可以调用 z 函数了: zepto.Z(dom, selector)init 的最后,将收集到的 dom 集合和对应的 selector 传入 Z 函数,返回 Z 对象。

zepto.fragment

zepto.fragment = function(html, name, properties) {
var dom, nodes, container
if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))
if (!dom) {
if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")
if (name === undefined) name = fragmentRE.test(html) && RegExp.$1
if (!(name in containers)) name = '*'
container = containers[name]
container.innerHTML = '' + html
dom = $.each(slice.call(container.childNodes), function() {
container.removeChild(this)
})
}
if (isPlainObject(properties)) {
nodes = $(dom)
$.each(properties, function(key, value) {
if (methodAttributes.indexOf(key) > -1) nodes[key](value)
else nodes.attr(key, value)
})
}
return dom
}

fragment 的作用的是将html片断转换成dom数组形式。

首先判断是否为标签的形式 singleTagRE.test(html) (如<div></div>), 如果是,则采用该标签名来创建dom对象 dom = $(document.createElement(RegExp.$1)),不用再作其他处理。singleTagRE = /^<(\w+)\s*\/?>(?:<\/\1>|)$/

如果尚未获取到 dom,接着进行:

if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")

这段是对 html 进行修复,如<p class="test" /> 修复成 <p class="test" /></p> 。正则表达式为 tagExpanderRE = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig

if (name === undefined) name = fragmentRE.test(html) && RegExp.$1

如果没有指定标签名,则获取标签名。如传入 <div>test</div> ,获取到的 namediv

if (!(name in containers)) name = '*'
container = containers[name]
container.innerHTML = '' + html
dom = $.each(slice.call(container.childNodes), function() {
container.removeChild(this)
})
}
// containers 已经开头定义,如下
table = document.createElement('table'),
tableRow = document.createElement('tr'),
containers = {
'tr': document.createElement('tbody'),
'tbody': table,
'thead': table,
'tfoot': table,
'td': tableRow,
'th': tableRow,
'*': document.createElement('div')
}

检测 name 是否为特殊的元素,如 tr 要用 tbody 包裹,其他的元素用 div 包裹。包裹元素的 childNodes 即为所需要获取的 dom

if (isPlainObject(properties)) {
nodes = $(dom)
$.each(properties, function(key, value) {
if (methodAttributes.indexOf(key) > -1) nodes[key](value)
else nodes.attr(key, value)
})
}
// methodAttributes 在上面已经定义,定义如下
methodAttributes = ['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset']

如果属性值为纯对象,则给元素设置属性。

如果所需设置的属性,zepto已经定义了相应的方法,则调用zepto对应的方法,否则统一调用zepto的attr 方法设置属性。

最后将 dom 返回

系列文章

  1. 读Zepto源码之代码结构
  2. 读 Zepto 源码之内部方法
  3. 读Zepto源码之工具函数

参考

作者:对角另一面

读 Zepto 源码之神奇的 $的更多相关文章

  1. 读Zepto源码之集合操作

    接下来几个篇章,都会解读 zepto 中的跟 dom 相关的方法,也即源码 $.fn 对象中的方法. 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码 ...

  2. 读 Zepto 源码之集合元素查找

    这篇依然是跟 dom 相关的方法,侧重点是跟集合元素查找相关的方法. 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的源码为 zept ...

  3. 读Zepto源码之操作DOM

    这篇依然是跟 dom 相关的方法,侧重点是操作 dom 的方法. 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的源码为 zepto1 ...

  4. 读Zepto源码之样式操作

    这篇依然是跟 dom 相关的方法,侧重点是操作样式的方法. 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的源码为 zepto1.2. ...

  5. 读Zepto源码之属性操作

    这篇依然是跟 dom 相关的方法,侧重点是操作属性的方法. 读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的源码为 zepto1.2. ...

  6. 读Zepto源码之Event模块

    Event 模块是 Zepto 必备的模块之一,由于对 Event Api 不太熟,Event 对象也比较复杂,所以乍一看 Event 模块的源码,有点懵,细看下去,其实也不太复杂. 读Zepto源码 ...

  7. 读Zepto源码之Callbacks模块

    Callbacks 模块并不是必备的模块,其作用是管理回调函数,为 Defferred 模块提供支持,Defferred 模块又为 Ajax 模块的 promise 风格提供支持,接下来很快就会分析到 ...

  8. 读Zepto源码之Deferred模块

    Deferred 模块也不是必备的模块,但是 ajax 模块中,要用到 promise 风格,必需引入 Deferred 模块.Deferred 也用到了上一篇文章<读Zepto源码之Callb ...

  9. 读Zepto源码之Ajax模块

    Ajax 模块也是经常会用到的模块,Ajax 模块中包含了 jsonp 的现实,和 XMLHttpRequest 的封装. 读 Zepto 源码系列文章已经放到了github上,欢迎star: rea ...

随机推荐

  1. 走入PHP-初次见面

    FROM:实验楼 Linux启动WEB服务器: $ sudo service apache2 start 新建并打开test.php文件: $ sudo gvim /path/test.php PHP ...

  2. 时效性福利:MindManager2017 破解攻略

    本文目的只是为了长期关注公众的活粉来谋个福利,24小时失效,没有提供盗版的意思 本文贡献的链接只存放2天,要下载的请从速~ 经过几个小时的奋斗,终于搞定了他,逆天我也终于可以从2016升级至2017~ ...

  3. C#研究OpenXML之路(3-OpenXMLSDKToolV25.msi)

    一.OpenXMLSDKToolV25.msi 看了几天的OpenXml,感觉如果完全手写代码,将会是一件非常苦逼的事情,即要分析对应xlsx文件层次结构,以及包含的xml文件的xml标签结构,还要关 ...

  4. 老李谈JVM内存模型

    老李谈JVM内存模型   poptest是国内唯一一家培养测试开发工程师的培训机构,以学员能胜任自动化测试,性能测试,测试工具开发等工作为目标.如果对课程感兴趣,请大家咨询qq:908821478,咨 ...

  5. android开发之-Android 开发之4.0界面设计原则-整理

    设计原则: 一.让人着迷: 1.给人惊喜:使用漂亮的界面.精心的动画.适时的音乐. 2.真实的对象比按钮和菜单更有趣   这句话的意思是:使用描述描述性的图标作为快捷方式,界面美观   当然这个快捷方 ...

  6. Bar 3D 和Pie 3D的统计图形

    最近在做一个关于图形统计的界面,主要用到的是Dev控件ChartControl(功能很强大,能解决基本和复杂图形统计问题). ChartControl主要有Diagram.Series.Legend三 ...

  7. C#, VB.NET如何将Excel转换为PDF

    在日常工作中,我们经常需要把Excel文档转换为PDF文档.你是否在苦恼如何以C#, VB.NET编程的方式将Excel文档转换为PDF文档呢?你是否查阅了许多资料,运用了大量的代码,但转换后的效果依 ...

  8. HibernateTemplate的使用

    HibernateTemplate 提供了非常多的常用方法来完成基本的操作,比如增加.删除.修改及查询等操作,Spring 2.0 更增加对命名 SQL 查询的支持,也增加对分页的支持.大部分情况下, ...

  9. setTimeout 和 setInteval 的区别。

    学习前端的可能都知道js有2个定时器setTimeOut和setinteval.用的时候可能不是很在意,但是2者还是有区别的 setTimeout方法是定时程序,也就是在什么时间以后干什么.干完就完了 ...

  10. 人生第一次hash

    人生的第一次hash交给了模板题. 讲道理,还没有别人快排要快,就比暴力快那么一点... 难道我写的hash就那么菜么? 我想了想,光是处理字符串就O(n*len).. 这是hash的正确写法吗?我都 ...