首页 > 技术文章 > zepto源码学习-02 工具方法-详细解读

Bond 2014-12-31 17:24 原文

上一篇:地址

先解决上次留下的疑问,开始看到zepto.z[0]这个东西的时候,我很是不爽,看着它都不顺眼,怎么一个zepto的实例对象var test1=$('#items');  test__proto__ 指向的是zepto.z[0];之前看到过如下代码 

    zepto.Z.prototype = Z.prototype = $.fn
    // Export internal API functions in the `$.zepto` namespace
    zepto.uniq = uniq
    zepto.deserializeValue = deserializeValue
    $.zepto = zepto
    //返回内部的$对象
    return $

 根据以上代码可以得出出test1__proto__===$.fn 应该是true

 既是zepto.z[0]就是$.fn

最终找到如下代码。代码上有英文注释, 让对象有看起来是数组,内部还给其加上了很多数组的行为。

$.fn ={} 但是constructor指向了zepto.Z ,还加了一个length: 0,

我尝试去掉length=0这个属性,得到如下结果

 

此时不再是zepto.z[0],而是zepto.z

如果再把constructor去掉,此时test__proto__指向Object; 其实最开始的时候$.fn={},本来就是赋值一个Object对象,只是手动改变了内部的constructor加上了length,

 

我决定来模拟一把,结构和zepto大体相同,声明了相应的c、z、fn。最后创建一个实例对象d,d的__proto__也指向了z[0];

这里折腾了一下,我发现如果fn这个Object对象,缺少forEach: emptyArray.forEach,reduce: emptyArray.reduce,push: emptyArray.push,sort: emptyArray.sort,splice: emptyArray.splice,indexOf: emptyArray.indexOf,其中的一些方法就不会显示z[0],直接会是z,感觉怪怪的。具体没去研究是哪几个方法。z和z[0]明显是不同的,一个是数组,一个就是普通对象。

进入本期主题,工具方法

先看官网上的API

 $()这个不是工具方法,$.camelCase

$.camelCase('hello-there') //=> "helloThere"
$.camelCase('helloThere') //=> "helloThere"

内部有很多地方均要调用这个方法,实现很简单基本,正则匹配替换 /-+(.)?/g, 前面可以有多个”-“后面(.)匹配一个字符,g全局匹配。

camelize = function(str) {
        return str.replace(/-+(.)?/g, function(match, chr) {
            return chr ? chr.toUpperCase() : ''
        })
    }

 

$.contains

 检查父节点是否包含给定的dom节点。

    //父元素是否有某个子元素,原生有的支持contains就调用element.contains,不支持就循环判断子节点的父节点
    $.contains = document.documentElement.contains ?
        function(parent, node) {
            return parent !== node && parent.contains(node)
        } :
        function(parent, node) {
            while (node && (node = node.parentNode))
                if (node === parent) return true
            return false
        }

$.each

 $.each方法用于遍历数组和对象,然后返回原始对象。它接受两个参数,分别是数据集合和回调函数,。回调函数返回 false 时停止遍历。 一般我们在不断的使用for的时候我们就可以考虑是否需要使用each来干掉for。

 

    $.each = function(elements, callback) {
        var i, key
        //判断是否是数组
        if (likeArray(elements)) {
            //为什么不 缓存length,这样每次都得去取 elements.length
            for (i = 0; i < elements.length; i++)
                if (callback.call(elements[i], i, elements[i]) === false) return elements
        } else {
            for (key in elements)
                if (callback.call(elements[key], key, elements[key]) === false) return elements
        }
        return elements
    }
  function likeArray(obj) {
        return typeof obj.length == 'number'
    }
 

 $.extend

$.extend(target, [source, [source2, ...]]) ⇒ target
$.extend(true, target, [source, ...]) ⇒ target v1.0+
通过源对象扩展目标对象的属性,源对象属性将覆盖目标对象属性。

默认情况下为,复制为浅拷贝(浅复制)。如果第一个参数为true表示深度拷贝(深度复制)。有的人没搞清楚深拷贝和浅拷贝的意思,结果掉坑里了。

    function extend(target, source, deep) {
        for (key in source)
            //深拷贝 并且source[key]是Object或者数组;====source[key] source[key] 可以提前缓存,每次写着不累啊。zepto的代码始终看着不够爽,个人习惯吧
            if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
                //如果source[key]是纯对象 但是target[key]不是纯对象
                if (isPlainObject(source[key]) && !isPlainObject(target[key]))
                    target[key] = {}
                //source[key]是数组 但是target[key]不是数组
                if (isArray(source[key]) && !isArray(target[key]))
                    target[key] = []
                //递归
                extend(target[key], source[key], deep)
                //不是深拷贝直接赋值给target
            } else if (source[key] !== undefined) target[key] = source[key]
    }

    // Copy all but undefined properties from one or more
    // objects to the `target` object.
    $.extend = function(target) {
        var deep,
        //转为数组
         args = slice.call(arguments, 1)
         //第一个参数为boolean类型
        if (typeof target == 'boolean') {
            deep = target
            //取到args第一个对象
            target = args.shift()
        }
        args.forEach(function(arg) {
            extend(target, arg, deep)
        })
        return target
    }

里面用到了isPlainObject其实$.isPlainObject 就是isPlainObject;改方法测试对象是否是“纯粹”的对象,这个对象是通过 对象常量("{}") 或者 new Object 创建的,如果是,则返回true。

$.isPlainObject({})         // => true
$.isPlainObject(new Object) // => true
$.isPlainObject(new Date)   // => false
$.isPlainObject(window)     // => false

 isPlainObject,先判断是不是Object对象,然后在判断不是window对象,在判断原型是Object.prototype。 Object.getPrototypeOf(obj)用来获取对象的原型 。

    function isWindow(obj) {
        return obj != null && obj == obj.window
    }

    function isDocument(obj) {
        return obj != null && obj.nodeType == obj.DOCUMENT_NODE
    }

    function isObject(obj) {
        return type(obj) == "object"
    }

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

 观察发现 isObject 这个函数内部是return type(obj)=='object',我们还得研究type的实现,type函数代码如下

function type(obj) {
        return obj == null ? String(obj) :
            class2type[toString.call(obj)] || "object"
    }

分析type函数里面的逻辑:如果obj===null返回String(obj)===》'null';如果不等于null返回:class2type[toString.call(obj)] || "object"

先调用toString.call(obj)===>toString是什么东西 ===>是这个 class2type.toString

toString.call(obj)就行相当于({}).toString.call(obj)。请看下图

 

 type函数其实就是({}).toString.call(obj) 得到obj的真实类型,然后再到class2type对象中去找对应的值,如果没找到则返回 "object"

那么class2type是个什么东西,有必要看一下,经过寻找,发现以下代码

// Populate the class2type map
    $.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
        class2type["[object " + name + "]"] = name.toLowerCase()
    })

  比如({}).toString.call({})==>[object Object] 

那么在class2type中应该是这样的 {"[object Object]": "object"},最终我们查看zepto内部class2type的值,如下

 

 如果我们自己去判断一个Object的真实类型会这样写:if(({}).toString.call(obj)==='[object Date]')//判断时间。 这样写明显麻烦,所以zepto内部给我们做了封装,我们只需要这样写  if($.type(obj)==='date') //这样是不是很简洁

我们平时直接试用$.type 感觉是很有好的,主要是zepto内部帮我们做了一些封装,JQuery这里的实现是一样滴。

$.trim

 这个基本上没说的,司徒正美那本《Javascript框架设计》讲了十多种字符串trim的实现,神一般的实现,有兴趣可以去看看;传送门

 

$.grep 

 获取一个新数组,新数组只包含回调函数中返回 ture 的数组项

 

$.grep = function(elements, callback) {
        return filter.call(elements, callback)
    }

内部其实就是调用了filter,filter又和not扯上了关系,为什么会这样设计,其实都是都为了代码重用。

filter: function(selector) {
            if (isFunction(selector)) return this.not(this.not(selector))
            return $(filter.call(this, function(element) {
                return zepto.matches(element, selector)
            }))
        },
not: function(selector) {
            var nodes = []
            if (isFunction(selector) && selector.call !== undefined)
                this.each(function(idx) {
                    if (!selector.call(this, idx)) nodes.push(this)
                })
            else {
                var excludes = typeof selector == 'string' ? this.filter(selector) :
                    (likeArray(selector) && isFunction(selector.item)) ? slice.call(selector) : $(selector)
                this.forEach(function(el) {
                    if (excludes.indexOf(el) < 0) nodes.push(el)
                })
            }
            return $(nodes)
        },

最核心的也就这句

this.each(function(idx) {
  if (!selector.call(this, idx)) nodes.push(this)
})

nodes是一个新的数组,把当前的值传个外部指定的回到函数,如果函数返回true就加到nodes中去,最后返回nodes这个新数组

$.inArray $.isArray $.isFunction $.isWindow $.parseJSON

 以上几个太简单了基本上没啥说的

 最后看一个map

$.map

 通过遍历集合中的元素,返回通过迭代函数的全部结果,null 和 undefined 将被过滤掉。代码实现基本都是很常规的没有什么技巧,也是先判断传入的对象是Object还是Array

    $.map = function(elements, callback) {
        var value, values = [],
            i, key
        if (likeArray(elements))
            for (i = 0; i < elements.length; i++) {
                value = callback(elements[i], i)
                if (value != null) values.push(value)
            } else
                for (key in elements) {
                    value = callback(elements[key], key)
                    if (value != null) values.push(value)
                }
        return flatten(values)
    }

最后还调用了flatten这个函数。 如果数组长度大于0 则调用$.fn.concat.apply([], array) ,那么在$.fn.concat中的this指向[], 函数参数就是array

concat的实现:遍历arguments,其实就是外面传如的array,挨个判断是不是Zepto对象,如果是value.toArray()就调用它的toArray方法。 最后是concat.apply(zepto.isZ(this) ? this.toArray() : this, args),这里同理,判断当前的this是不是zepto对象,如果是调用toArray方法,不是就传入this。最终把数组链接起来,返回给外部。

function flatten(array) {
        return array.length > 0 ? $.fn.concat.apply([], array) : array
    }

concat: function() {
            var i, value, args = []
            for (i = 0; i < arguments.length; i++) {
                value = arguments[i]
                args[i] = zepto.isZ(value) ? value.toArray() : value
            }
            return concat.apply(zepto.isZ(this) ? this.toArray() : this, args)
        }
emptyArray = [],
concat = emptyArray.concat,    
this.toArray()的实现如下:toArray调用get方法,之前说过 this其实就是个维数组,这里转换成数组,里面的元素都是原生的dom对象。当然这里可以传入下标,可以取得对应的原生dom对象。
get: function(idx) {
            return idx === undefined ? slice.call(this) : this[idx >= 0 ? idx : idx + this.length]
        },
        toArray: function() {
            return this.get()
        },

 看来一个map方法还是挺复杂的,里面各种处理逻辑,牵扯一堆的东西。为什么会这样 Why?? 等以后分析后面的代码就知道了,很多地方要用到map,为了重用,所以这个map设计成这样了!!

看来这个工具方法还是可以扯很多,其实还省略了不少呢!今天到此为止!

本文地址:http://www.cnblogs.com/Bond/articles/4195800.html 

 

推荐阅读