Zepto源码分析——Zepto选择器

选择器引擎是框架中实现操作的主要方式,可以快速选取到所需元素,通过我们更加熟知的css选择器,zepto的选择器分两大块,一块在zepto.js中的核心选择器qsa方法,另外是selector.js文件,里边封装有一些扩充选择器的实现。

选择器引擎概述

CSS选择符是一条CSS样式中最左边的部分,选择符分为了五大类:元素、关系、伪类、并联、伪元素。其中只有伪元素选择器不能直接被js所选取到。

一般认为,框架的选择器引擎需要包括以下几个基本方法:

  • contain(a,b):判断a中是否包含b,主要用作优化
  • visible()与hidden():判断是否可见
  • selected():选中了元素
  • sortNode():节点的排序与去重,主要为了使更类似于原生方法的排序
  • filter():过滤器,对于不支持querySelectorAll的游览器,需要对用户的api进行过滤,这个步骤类似词法分析,可以拆分出有用的选择符,对其使用应该的API

不过zepto比较奇葩的一点是,zepto主要面向移动端,移动端游览器坑少,所以zepto就直接拿querySelectorAll来匹配非规定外的元素了,好处自不必说,代码少实现简单。坏处就是兼容性不好,也学不到真正选择器引擎的核心,但zepto的优化做的还是很好,也是值得看。而且querySelectorAll这也是未来的趋势。

Zepto核心选择器

首先是选择器的实现:

zepto.qsa = function (element, selector) {
    var found,
    maybeID = selector[0] == '#',
    maybeClass = !maybeID && selector[0] == '.',
    nameOnly = maybeID || maybeClass ? selector.slice(1) : selector, // Ensure that a 1 char tag name still gets checked
    isSimple = simpleSelectorRE.test(nameOnly)
  return (element.getElementById && isSimple && maybeID) ? // Safari DocumentFragment doesn't have getElementById
    ((found = element.getElementById(nameOnly)) ? [found] : []) :
    (element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
    slice.call(
      isSimple && !maybeID && element.getElementsByClassName ? // DocumentFragment doesn't have getElementsByClassName/TagName
      maybeClass ? element.getElementsByClassName(nameOnly) : // If it's simple, it could be a class
      element.getElementsByTagName(selector) : // Or a tag
      element.querySelectorAll(selector) // Or it's not simple, and we need to query all
    )
}

判断了选择器类型,是id、class、标签还是复杂选择器,然后分别调用getElementById()、getElementsByClassName()、getElementsByTagName()、querySelectorAll()。

其次是否匹配选择器:

zepto.matches = function (element, selector) {
    if (!selector || !element || element.nodeType !== 1) return false
    var matchesSelector = element.matches || element.webkitMatchesSelector ||
      element.mozMatchesSelector || element.oMatchesSelector ||
      element.matchesSelector
    if (matchesSelector) return matchesSelector.call(element, selector)
    // fall back to performing a selector:
    var match, parent = element.parentNode,
      temp = !parent
    if (temp)(parent = tempParent).appendChild(element)
    match = ~zepto.qsa(parent, selector).indexOf(element)
    temp && tempParent.removeChild(element)
    return match
  }

主要是用来判断当前DOM节点否能完全匹配对应的CSS选择器规则。这个matches方法可以在事件委托等地方被用得上,用来判定匹配到当前标签的元素,当匹配到后添加事件。但原生的兼容性很差,因此框架在这里做了兼容性处理。

Zepto拓展选择器

把全部文件站过来,将源码解读写到注释中

(function ($) {
    var zepto = $.zepto,
        oldQsa = zepto.qsa,
        oldMatches = zepto.matches
    function visible(elem) {
        elem = $(elem)
        return !!(elem.width() || elem.height()) && elem.css("display") !== "none"
    }
    //这是一套过滤器系统
    var filters = $.expr[':'] = {
        visible: function () {
            if (visible(this))
                return this
        },
        hidden: function () {
            if (!visible(this))
                return this
        },
        selected: function () {
            if (this.selected)
                return this
        },
        checked: function () {
            if (this.checked)
                return this
        },
        parent: function () {
            return this.parentNode
        },
        first: function (idx) {
            if (idx === 0)
                return this
        },
        last: function (idx, nodes) {
            if (idx === nodes.length - 1)
                return this
        },
        eq: function (idx, _, value) {
            if (idx === value)
                return this
        },
        contains: function (idx, _, text) {
            if ($(this).text().indexOf(text) > -1)
                return this
        },
        has: function (idx, _, sel) {
            if (zepto.qsa(this, sel).length)
                return this
        }
    }

    var filterRe = new RegExp('(.*):(\\w+)(?:\\(([^)]+)\\))?$\\s*'),
        childRe = /^\s*>/,
        classTag = 'Zepto' + (+ new Date())
    //分解选择器为三部分,选择器、选择器的过滤器方法、参数
    function process(sel, fn) {
        // quote the hash in `a[href^=#]` expression
        sel = sel.replace(/=#\]/g, '="#"]')
        var filter,
            arg,
            match = filterRe.exec(sel)
        if (match && match[2] in filters) {
            filter = filters[match[2]],
                arg = match[3]
            sel = match[1]
            if (arg) {
                var num = Number(arg)
                if (isNaN(num))
                    arg = arg.replace(/^["']|["']$/g, '')
                else
                    arg = num
            }
        }
        return fn(sel, filter, arg)
    }

    zepto.qsa = function (node, selector) {
        return process(selector, function (sel, filter, arg) {
            try {
                var taggedParent
                if (!sel && filter)
                    sel = '*'
                else if (childRe.test(sel))
                // support "> *" child queries by tagging the parent node with a unique class
                // and prepending that classname onto the selector
                    taggedParent = $(node).addClass(classTag),
                        sel = '.' + classTag + ' ' + sel

                var nodes = oldQsa(node, sel)
            } catch (e) {
                console.error('error performing selector: %o', selector)
                throw e
            } finally {
                if (taggedParent)
                    taggedParent.removeClass(classTag)
            }
            return !filter
                ? nodes
                : zepto.uniq($.map(nodes, function (n, i) {
                    return filter.call(n, i, nodes, arg)
                }))
        })
    }

    zepto.matches = function (node, selector) {
        return process(selector, function (sel, filter, arg) {
            return (!sel || oldMatches(node, sel)) && (!filter || filter.call(node, null, arg) === node)
        })
    }
})(Zepto)

从源码看出,虽然zepto主要采用的是getElementById()、getElementsByClassName()、getElementsByTagName()、querySelectorAll()这些内置的api完成,思路是分解->匹配->调用原生api->组装zepto对象。

根据不同的情况,给出了不同的提速方案,getElementById是最优先的,因为该api内部做了缓存而且只返回一个节点;getElementsByClassName()、getElementsByTagName()也是比较快的,返回多个节点并且又缓存;只有无法完成时候才进行querySelectorAll()。此外注意的是,getElementsByClassName()、getElementsByTagName()返回的是一个NodeList对象,而querySelectorAll返回的是一个StaticNodeList对象,前者动态后者静态,前者每次匹配到的都是同是缓存引用,后者返回的是不同的Object对象,数据表明前者的速度要快百分之90%以上,这就是为什么尽量用getElementsByClassName()、getElementsByTagName()的原因。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

😉😐😡😈🙂😯🙁🙄😛😳😮:mrgreen:😆💡😀👿😥😎😕