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

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

源码版本

本文阅读的源码为 zepto1.2.0

zepto的css选择器 zepto.qsa

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

源码

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

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

  1. 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 ,如果不为以上三种类型,直接返回 []

终极三元表达式

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

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

zepto.Z 函数

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

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

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

  1. function Z(dom, selector) {
  2. var i, len = dom ? dom.length : 0
  3. for (i = 0; i < len; i++) this[i] = dom[i]
  4. this.length = len
  5. this.selector = selector || ''
  6. }

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

zepto.isZ

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

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

$的实现 zepto.init 函数

$的实现

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

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

zepto.init

  1. zepto.init = function(selector, context) {
  2. var dom // dom 集合
  3. if (!selector) return zepto.Z() // 分支1
  4. else if (typeof selector == 'string') { // 分支2
  5. selector = selector.trim()
  6. if (selector[0] == '<' && fragmentRE.test(selector))
  7. dom = zepto.fragment(selector, RegExp.$1, context), selector = null
  8. else if (context !== undefined) return $(context).find(selector)
  9. else dom = zepto.qsa(document, selector)
  10. }
  11. else if (isFunction(selector)) return $(document).ready(selector) // 分支3
  12. else if (zepto.isZ(selector)) return selector // 分支4
  13. else { // 分支5
  14. if (isArray(selector)) dom = compact(selector)
  15. else if (isObject(selector))
  16. dom = [selector], selector = null
  17. else if (fragmentRE.test(selector))
  18. dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null
  19. else if (context !== undefined) return $(context).find(selector)
  20. else dom = zepto.qsa(document, selector)
  21. }
  22. return zepto.Z(dom, selector)
  23. }

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

$的用法

  1. $(selector, [context]) collection // 用法1
  2. $(<Zepto collection>) same collection // 用法2
  3. $(<DOM nodes>) collection // 用法3
  4. $(htmlString) collection // 用法4
  5. $(htmlString, attributes) collection v1.0+ // 用法5
  6. 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

  1. zepto.fragment = function(html, name, properties) {
  2. var dom, nodes, container
  3. if (singleTagRE.test(html)) dom = $(document.createElement(RegExp.$1))
  4. if (!dom) {
  5. if (html.replace) html = html.replace(tagExpanderRE, "<$1></$2>")
  6. if (name === undefined) name = fragmentRE.test(html) && RegExp.$1
  7. if (!(name in containers)) name = '*'
  8. container = containers[name]
  9. container.innerHTML = '' + html
  10. dom = $.each(slice.call(container.childNodes), function() {
  11. container.removeChild(this)
  12. })
  13. }
  14. if (isPlainObject(properties)) {
  15. nodes = $(dom)
  16. $.each(properties, function(key, value) {
  17. if (methodAttributes.indexOf(key) > -1) nodes[key](value)
  18. else nodes.attr(key, value)
  19. })
  20. }
  21. return dom
  22. }

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

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

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

  1. 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

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

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

  1. if (!(name in containers)) name = '*'
  2. container = containers[name]
  3. container.innerHTML = '' + html
  4. dom = $.each(slice.call(container.childNodes), function() {
  5. container.removeChild(this)
  6. })
  7. }
  8. // containers 已经开头定义,如下
  9. table = document.createElement('table'),
  10. tableRow = document.createElement('tr'),
  11. containers = {
  12. 'tr': document.createElement('tbody'),
  13. 'tbody': table,
  14. 'thead': table,
  15. 'tfoot': table,
  16. 'td': tableRow,
  17. 'th': tableRow,
  18. '*': document.createElement('div')
  19. }

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

  1. if (isPlainObject(properties)) {
  2. nodes = $(dom)
  3. $.each(properties, function(key, value) {
  4. if (methodAttributes.indexOf(key) > -1) nodes[key](value)
  5. else nodes.attr(key, value)
  6. })
  7. }
  8. // methodAttributes 在上面已经定义,定义如下
  9. 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. python运用中文注释时报错解决方法

    写了一段简单的代码,不知 为什么总是报错,后来上网查了一下才知道原因,当用中文进行注释时需要添加如下代码:# coding=utf-8          (注意:该段代码必须放在最前面才能有用,并且 ...

  2. IOS各种手势操作实例

    先看下效果 手势相关的介绍 IOS中手势操作一般是 UIGestureRecognizer 类的几个手势子类去实现,一般我们用到的手势就这么5种: 1.点击  UITapGestureRecogniz ...

  3. How do you make an awesome team?(来考验一下自己的英语能力吧)

    How do you make an awesome team? I was talking to my awesome friend Amy Hanlon (who is a software en ...

  4. file_get_contents和curl对于post方式的解决办法

    post方式解决办法 其实很简单,我们只要仔细看看就知道了... file_get_contents: $content=$_POST['content'];$access_token=$_POST[ ...

  5. ngrok完成内网映射外网

    项目需求: 完成微信公众号开发配置搭建,其中需要一个可以外部访问的域名 实现技术: www.ngrok.cc 实现方式: 在该网站最下方下载需要的ngrok文件,在最上方点击注册,获取token,之后 ...

  6. Android中的java层的线程暂停和恢复实现

    /**  * 基础线程对象.  *  * @author jevan  * @version (1.0 at 2013-6-17)  * @version (1.1 at 2013-7-2) 增加on ...

  7. JDBC基础学习(五)—批处理插入数据

    一.批处理介绍      当需要成批插入或者更新记录时.可以采用Java的批量更新机制,这一机制允许多条语句一次性提交给数据库批量处理.通常情况下比单独提交处理更有效率. JDBC的批量处理语句包括下 ...

  8. Spark入门实战

    星星之火,可以燎原 Spark简介 Spark是一个开源的计算框架平台,使用该平台,数据分析程序可自动分发到集群中的不同机器中,以解决大规模数据快速计算的问题,同时它还向上提供一个优雅的编程范式,使得 ...

  9. iOS实现高斯模糊效果(Swift版本)

    给UIimage添加分类 extension UIImage { /// 高斯模糊 func gaussianBlur(var blurAmount:CGFloat) -> UIImage { ...

  10. vue-router2 使用

    VUE-ROUTER2  API http://router.vuejs.org/zh-cn/api/router-link.html   1,安装vue-router npm install vue ...