谦龙

2017-12-09 17:59

Zepto核心模块之工具方法拾遗

本文作者:IMWeb 谦龙 原文出处:IMWeb社区 未经同意,禁止转载

前言

平时开发过程中经常会用类似eachmapforEach之类的方法,Zepto本身也把这些方法挂载到$函数身上,作为静态方法存在,既可以给Zepto的实例使用,也能给普通的js对象使用。今天我们主要针对其提供的这些api做一些源码实现分析。

源码仓库 原文链接

<!-- more -->

具体各个api如何使用可以参照英文文档Zepto.js 中文文档Zepto.js

1. $.camelCase

该方法主要是将连字符转化成驼峰命名法。例如可以将a-b-c这种形式转换成aBC,当然连字符的数量可以是多个,a---b-----c => aBC,具体实现已经在这些Zepto中实用的方法集说过了,可以点击查看。而其代码也只是将camelize函数赋值给了$.camelCase

$.camelCase = camelize

2. $.contains

$.contains(parent, node) ⇒ boolean该方法主要用来检测parent是否包含给定的node节点。如果parent和node为同一节点,则返回false。

举例


<ul class="list">
  <li class="item">1</li>
  <li>2</li>
</ul>
<div class="test"></div>

let oList = document.querySelector('.list')
let oItem = document.querySelector('.item')
let oTest = document.querySelector('.test')

console.log($.contains(oList, oItem)) // true 父子节点
console.log($.contains(oList, oList)) // false 同一节点
console.log($.contains(oList, oTest)) // false 兄弟节点

源码

$.contains = document.documentElement.contains ?
  function (parent, node) {
    // 防止parent和node传相同的节点,故先parent !== node
    // 接着就是调用原生的contains方法判断了
    return parent !== node && parent.contains(node)
  } :
  function (parent, node) {
    // 当node节点存在,就把node的父节点赋值给node
    while (node && (node = node))
      // 如果node的父节点和parent相等就返回true,否则继续向上查找
      // 其实有一个疑问,为什么开头不先排查node === parent的情况呢
      // 不然经过循环最后却得到false,非常的浪费
      if (node === parent) return true
    return false
  }

用了document.documentElement.contains做判断,如果浏览器支持该方法,就用node.contains重新包了一层得到一个函数,差别就在于如果传入的两个节点相同,那么原生的node.contains返回true,具体用法可以查看MDN Node.contains但是$.contains返回false

如果原生不支持就需要我们自己写一个方法了。主要逻辑还是通过一个while循环,判断传入的node节点的父节点是否为parent,如果一个循环下来,还不是最后才返回false

其实这里应该是可以做一个优化的,一进来的时候就先判断两个节点是否为同一节点,不是再进行后续的判断

3. $.each

用来遍历数组或者对象,类似原生的forEach但是不同的是,可以中断循环的执行,并且服务对象不局限于数组。

举例


let testArr = ['qianlongo', 'fe', 'juejin']
let testObj = {
  name: 'qianlongo',
  sex: 'boy'
}

$.each(testArr, function (i, val) {
  console.log(i, val)
})

// 0 "qianlongo"
// 1 "fe"
// 2 "juejin"

$.each(testObj, function (key, val) {
  console.log(key, val)
})

// name qianlongo
// sex boy

需要注意的是,此时回调函数中的this指向的就是数组或者对象的某一项。这样主要是方便内部的一些其他方法在遍历dom节点的时候,this很方便地就指向了对应的dom

源码实现

$.each = function (elements, callback) {
  var i, key
  // 如果是类数组就走这个if
  if (likeArray(elements)) {
    for (i = 0; i < elements.length; i++)
      // 可以看到用.call去执行了callback,并且第一个参数是数组中的item
      // 如果用来遍历dom,那么内部的this,指的就是当前这个元素本身
      // 判断callback执行的结果,如果是false,就中断遍历
      // 中断遍历这就是和原生forEach不同的地方
      // 2017-8-16添加,原生的forEach内部的this指向的是数组本身,但是这里指向的是数组的项
      // 2017-8-16添加,原生的forEach回调函数的参数是val, i...,这里反过来
      if (callback.call(elements[i], i, elements[i]) === false) return elements
  } else {
    // 否则回去走for in循环,逻辑与上面差不多
    for (key in elements)
      if (callback.call(elements[key], key, elements[key]) === false) return elements
  }

  return elements
}

likeArray已经在这些Zepto中实用的方法集说过了,可以点击查看。

4. $.extend

Zepto中提供的拷贝方法,默认为浅拷贝,如果第一个参数为布尔值则表示深拷贝。

源码实现


$.extend = function (target) {
  // 将第一个参数之外的参数变成一个数组
  var deep, args = slice.call(arguments, 1)
  // 处理第一个参数是boolean值的情况,默认是浅复制,深复制第一个参数传true
  if (typeof target == 'boolean') {
    deep = target
    target = args.shift()
  }
  // $.extend(true, {}, source1, source2, source3)
  // 有可能有多个source,遍历调用内部extend方法,实现复制
  args.forEach(function (arg) { extend(target, arg, deep) })
  return target
}

可以看到首先对第一个参数是否为布尔值进行判断,有意思的是,只要是布尔值都表示深拷贝,你传true或者false都是一个意思。接着就是对多个source参数进行遍历调用内部方法extend

接下来我们主要来看内部方法extend

function extend(target, source, deep) {
  // 对源对象source进行for in遍历
  for (key in source)
    // 如果source[key]是纯对象或者数组,并且指定为深复制
    if (deep && (isPlainObject(source[key]) || isArray(source[key]))) {
      // 如果source[key]为纯对象,但是target[key]不是纯对象,则将目标对象的key设置为空对象
      if (isPlainObject(source[key]) && !isPlainObject(target[key]))
        target[key] = {}
      // 如果  如果source[key]为数组,但是target[key]不是数组,则将目标对象的key设置为数组
      if (isArray(source[key]) && !isArray(target[key]))
        target[key] = []
      // 递归调用extend函数  
      extend(target[key], source[key], deep)
    }
    // 浅复制或者source[key]不为undefined,便进行赋值
    else if (source[key] !== undefined) target[key] = source[key]
}

整体实现其实还挺简单的,主要是遇到对象或者数组的时候,并且指定为深赋值,则递归调用extend本身,从而完成复制过程。

5. $.grep

其实就是数组的原生方法filter,最终结果得到的是一个数组,并且只包含回调函数中返回 true 的数组项

直接看源码实现


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

通过call形式去调用原生的数组方法 filter,过滤出符合条件的数据项。

6. $.inArray

返回数组中指定元素的索引值,没有找到该元素则返回-1,fromIndex是一个可选的参数,表示从哪个地方开始往后进行查找。

$.inArray(element, array, [fromIndex]) ⇒ number

举例

let testArr = [1, 2, 3, 4]

console.log($.inArray(1, testArr)) // 0
console.log($.inArray(4, testArr)) // 3
console.log($.inArray(-10, testArr)) // -1
console.log($.inArray(1, testArr, 2)) // -1

源码实现

$.inArray = function (elem, array, i) {
  return emptyArray.indexOf.call(array, elem, i)
}

可见其内部也是调用的原生indexOf方法。

7. $.isArray

判断obj是否为数组。

我们知道判断一个值是否为对象,方式其实挺多的,比如下面的这几种方式

// 1. es5中的isArray

console.log(Array.isArray([])) // true

// 2. 利用instanceof判断

console.log([] instanceof Array) // true

// 3. 最好的方式 toString
console.log(Object.prototype.toString.call([]) === '[object Array]') // true

而Zepto中就是采用的第二种方式


var isArray = Array.isArray || function (object) {     return object instanceof Array
}

$.isArray = isArray

如果支持isArray方法就用原生支持的,否则通过instanceof判断,其实不太清楚为什么第二种方式,我们都知道这是有缺陷的,在有iframe场景下,就会出现判断不准确的情况.

8. $.isFunction

判断一个值是否为函数类型

源码实现

function isFunction(value) { 
  return type(value) == "function" 
}

$.isFunction = isFunction

主要还是通过内部方法type来实现的,详情可以点击这些Zepto中实用的方法集查看。

9. $.isNumeric

如果传入的值为有限数值或一个字符串表示的数字,则返回ture。

举例

$.isNumeric(null) // false
$.isNumeric(undefined) // false
$.isNumeric(true) // false
$.isNumeric(false) // false
$.isNumeric(0) // true
$.isNumeric('0') // true
$.isNumeric('') // false
$.isNumeric(NaN) // false
$.isNumeric(Infinity) // false
$.isNumeric(-Infinity) // false

源码

$.isNumeric = function (val) {
  var num = Number(val), type = typeof val
  return val != null && type != 'boolean' &&
    (type != 'string' || val.length) &&
    !isNaN(num) && isFinite(num) || false
}

首先val经过Number函数转化,得到num,然后获取val的类型得到type

我们来回顾一下Number(val)的转化规则,这里截取一张图。

Number转化规则

看起来转化规则非常复杂,但是有几点我们可以确定,

  1. 如果输入的是数字例如1,1.3那转化后的还是数字,
  2. 如果输入的是字符串数字类型例如'123', '12.3'那转化后的也是数字
  3. 如果输入的是空字符串''那转化后得到的是0
  4. 如果输入是类似字符串'123aaa',那转化后得到的是NaN

所以再结合下面的判断

  1. 通过val != null排除掉nullundefined

  2. 通过type != 'boolean'排除掉,truefalse

  3. 通过isFinite(num)限定必须是一个有限数值

  4. 通过!isNaN(num)排除掉被Number(val)转化为NaN的值

  5. (type != 'string' || val.length), val为字符串,并且字符串的长度大于0,排除''空字符串的场景。

以上各种判断下来基本就满足了这个函数原来的初衷要求。

9. $.isPlainObject

测试对象是否是“纯粹”的对象,这个对象是通过 对象常量("{}") 或者 new Object 创建的,如果是,则返回true

10. $.isWindow

如果object参数为一个window对象,那么返回true

该两个方法在这些Zepto中实用的方法集也聊过了,可以点击查看一下。

11. $.map

和原生的map比较相似,但是又有不同的地方,比如这里的map得到的记过有可能不是一一映射的,也就是可能得到比原来数组项数更多的数组,以及这里的map是可以用来遍历对象的。

我们先看几个例子

let testArr = [1, 2, null, undefined]
let resultArr1 = $.map(testArr, (val, i) => {
  return val
})
let resultArr2 = $.map(testArr, (val, i) => {
  return [val, [val]]
})

// 再来看看原生的map的表现
let resultArr3 = testArr.map((val, i) => {
  return val
})
let resultArr4 = testArr.map((val, i) => {
  return [val, [val]]
})

运行结果如下

可以看出

  1. resultArr1resultArr3的区别是$.mapundefinednull给过滤掉了。
  2. resultArr2resultArr4的区别是$.map把回调函数的返回值给铺平了。

接下来看看源码是怎么实现的。


 $.map = function (elements, callback) {
  var value, values = [], i, key
  // 如果是类数组,则用for循环
  if (likeArray(elements))
    for (i = 0; i < elements.length; i++) {
      value = callback(elements[i], i)
      // 如果callback的返回值不为null或者undefined,就push进values
      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)
}

从源码实现上可以看出因为value != null以及flatten(values)造成了上述差异。

12. $.noop

其实就是引用一个空的函数,什么都不处理,那它到底有啥用呢?

比如。我们定义了几个变量,他未来是作为函数使用的。


let doSomeThing = () => {}
let doSomeThingElse = () => {}

如果直接这样


let doSomeThing = $.noop
let doSomeThingElse = $.noop

宿主环境就不必为我们创建多个匿名函数了。

其实还有一种可能用的不多的场景,在判断一个变量是否是undefined的时候,可以用到。因为函数没有返回值,默认返回undefined,也就是排除了那些老式浏览器undefined可以被修改的情况


if (xxx === $.noop()) {
  // xxx
}

13. $.parseJSON

原生JSON.parse方法的别名,接收的是一个字符串对象,返回一个对象。

源码实现

$.parseJSON = JSON.parse

14. $.trim

删除字符串首尾的空白符,如果传入nullundefined返回空字符串

源码实现

$.trim = function (str) {
  return str == null ? "" : String.prototype.trim.call(str)
}

15. $.type

获取JavaScript对象的类型。可能的类型有: null undefined boolean number string function array date regexp object error.

该方法内部实现其实就是内部的type函数,并且已经在这些Zepto中实用的方法集聊过了,可以点击查看。

$.type = type

结尾

Zepto大部分工具方法或者说静态方法就是这些了,欢迎大家指正其中的错误和问题。

参考资料

读zepto源码之工具函数

MDN trim

MDN typeof

MDN isNaN

MDN Number

MDN Node.contains

文章记录

  1. 原来你是这样的jsonp(原理与具体实现细节)

  2. 谁说你只是"会用"jQuery?

  3. 向zepto.js学习如何手动触发DOM事件

  4. mouseenter与mouseover为何这般纠缠不清?

  5. 这些Zepto中实用的方法集

0条评论

    您需要 注册 一个IMWeb账号或者 才能进行评论。