资讯专栏INFORMATION COLUMN

Zepto这样操作元素属性

付伦 / 1098人阅读

摘要:还有一点需要注意的是方法设置或者获取都是在操作元素的属性,那它和,的区别在哪呢可以查看设置设置与的设置部分比较类似,既支持直接传入普通的字符串也支持传入回调函数。

前言

使用Zepto的时候,我们经常会要去操作一些DOM的属性,或元素本身的固有属性或自定义属性等。比如常见的有attr(),removeAttr(),prop(),removeProp(),data()等。接下来我们挨个整明白他们是如何实现的...点击zepto模块源码注释查看这篇文章对应的解析。

原文链接

源码仓库

attr()

读取或设置dom的属性。

如果没有给定value参数,则读取对象集合中第一个元素的属性值。

当给定了value参数。则设置对象集合中所有元素的该属性的值。当value参数为null,那么这个属性将被移除(类似removeAttr),多个属性可以通过对象键值对的方式进行设置。zeptojs_api/#attr

示例

// 获取name属性
attr(name)   
// 设置name属性        
attr(name, value)
// 设置name属性,不同的是使用回调函数的形式
attr(name, function(index, oldValue){ ... })
// 设置多个属性值
attr({ name: value, name2: value2, ... })

已经知道了如何使用attr方法,在开始分析attr实现源码之前,我们先了解一下这几个函数。

setAttribute

function setAttribute(node, name, value) {
  value == null ? node.removeAttribute(name) : node.setAttribute(name, value)
}

它的主要作用就是设置或者删除node节点的属性。当value为null或者undefined的时候,调用removeAttribute方法移除name属性,否则调用setAttribute方法设置name属性。

funcArg

function funcArg(context, arg, idx, payload) {
  return isFunction(arg) ? arg.call(context, idx, payload) : arg
}

funcArg函数在多个地方都有使用到,主要为类似attr,prop,val等方法中第二个参数可以是函数或其他类型提供可能和便捷。

如果传入的arg参数是函数类型,那么用context作为arg函数的执行上下文,以及将idx和payload作为参数去执行。否则直接返回arg参数。

好啦接下来开始看attr的源码实现了

attr: function (name, value) {
  var result
  return (typeof name == "string" && !(1 in arguments)) ?
    // 获取属性
    (0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined) :
    // 设置属性
    this.each(function (idx) {
      if (this.nodeType !== 1) return
      // 设置多个属性值
      if (isObject(name)) for (key in name) setAttribute(this, key, name[key])
      // 设置一个属性值
      else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
    })
}

代码分为两部分,获取与设置属性。先看

获取部分

typeof name == "string" && !(1 in arguments)) ?
    // 获取属性
    (0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined) : "设置代码逻辑代码块"

当name参数是string类型,并且没有传入value参数时候,意味着是读取属性的情况。紧接着再看当前选中的元素集合中第一个元素是否存在并且节点类型是否为element类型,如果是,再调用getAttribute获取name属性,结果不为null或者undefined的话直接返回,否则统一返回undefined。

设置部分

this.each(function (idx) {
  if (this.nodeType !== 1) return
  // 设置多个属性值
  if (isObject(name)) for (key in name) setAttribute(this, key, name[key])
  // 设置一个属性值
  else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
})

调用each方法,对当前的元素集合进行遍历操作,遍历过程中,如果当前的元素不是element类型,直接return掉。否则根据name参数传入的是否是对象进行两个分支的操作。

如果name是个对象,那对对象进行遍历,再挨个调用setAttribute方法,进行属性设置操作。

不是对象的话,接下来的这行代码,让第二个参数既可以传入普通的字符串,也可以传入回调函数。

看个实际使用的例子

$(".box").attr("name", "qianlongo")

$(".box").attr("name", function (idx, oldVal) {
  return oldVal + "qianlongo"
})

可以看到如果传入的是回调函数,那回调函数可以接收到元素的索引,以及要设置的属性的之前的值。

removeAttr()

移除当前对象集合中所有元素的指定属性,理论上讲attr也可以做到removeAttr的功能。只要将要移除的name属性设置为null或者undefined即可。

removeAttr: function (name) {
  return this.each(function () {
  // 通过将name分割,再将需要移除的属性进行遍历删除
  this.nodeType === 1 && name.split(" ").forEach(function (attribute) {
    setAttribute(this, attribute)
  }, this)
  })
}

代码本身很简单,对当前选中的元素集合进行遍历操作,然后对name参数进行空格分割(这样对于name传入类似"name sex age"就可以批量删除了),最后还是调用的setAttribute方法进行属性删除操作。

prop()

读取或设置dom元素的属性值,简写或小写名称,比如for, class, readonly及类似的属性,将被映射到实际的属性上,比如htmlFor, className, readOnly, 等等。

直接看源码实现

prop: function (name, value) {
  name = propMap[name] || name
  return (1 in arguments) ?
    this.each(function (idx) {
      this[name] = funcArg(this, value, idx, this[name])
    }) :
    (this[0] && this[0][name])
}

通过1 in arguments作为设置与获取元素属性的判断标志,value传了,则对当前选中的元素集合进行遍历操作,同样用到了funcArg函数,让value既可以传入函数,也可以传入其他值。与attr方法不同的是,因为是设置和获取元素的固有属性,所以直接向元素设置和读取值就可以了。

需要注意的是当你传入class,for等属性的时候需要被映射到className,htmlFor等,下面是映射列表

var propMap = {
  "tabindex": "tabIndex",
  "readonly": "readOnly",
  "for": "htmlFor",
  "class": "className",
  "maxlength": "maxLength",
  "cellspacing": "cellSpacing",
  "cellpadding": "cellPadding",
  "rowspan": "rowSpan",
  "colspan": "colSpan",
  "usemap": "useMap",
  "frameborder": "frameBorder",
  "contenteditable": "contentEditable"
}
removeProp()

从集合的每个DOM节点中删除一个属性

removeProp: function (name) {
  name = propMap[name] || name
  return this.each(function () { delete this[name] })
}

直接通过delete去删除,但是如果尝试删除DOM的一些内置属性,如className或maxLength,将不会有任何效果,因为浏览器禁止删除这些属性。

html()

获取或设置对象集合中元素的HTML内容。当没有给定content参数时,返回对象集合中第一个元素的innerHtml。当给定content参数时,用其替换对象集合中每个元素的内容。content可以是append中描述的所有类型zeptojs_api/#html

源码分析

html: function (html) {
  return 0 in arguments ?
    this.each(function (idx) {
      var originHtml = this.innerHTML
      $(this).empty().append(funcArg(this, html, idx, originHtml))
    }) :
    (0 in this ? this[0].innerHTML : null)
}

如果html传了,就遍历通过append函数设置html,没传就是获取(即返回当前集合的第一个元素的innerHTML)注意:这里的html参数可以是个函数,接收的参数是当前元素的索引和html。

text()

获取或者设置所有对象集合中元素的文本内容。

当没有给定content参数时,返回当前对象集合中第一个元素的文本内容(包含子节点中的文本内容)。

当给定content参数时,使用它替换对象集合中所有元素的文本内容。它有待点似 html,与它不同的是它不能用来获取或设置 HTMLtext

text() ⇒ string

text(content) ⇒ self

text(function(index, oldText){ ... }) ⇒ self

源码分析

text: function (text) {
  return 0 in arguments ?
    this.each(function (idx) {
      var newText = funcArg(this, text, idx, this.textContent)
      this.textContent = newText == null ? "" : "" + newText
    }) :
    (0 in this ? this.pluck("textContent").join("") : null)
}

同样包括设置和获取两部分,判断的边界则是是否传入了第一个参数。先看获取部分。

获取text

(0 in this ? this.pluck("textContent").join("") : null)

0 in this 当前是否选中了元素,没有直接返回null,有则通过this.pluck("textContent").join("")获取,我们先来看一下pluck做了些什么

plunck

// `pluck` is borrowed from Prototype.js
pluck: function (property) {
  return $.map(this, function (el) { return el[property] })
},

pluck也是挂在原型上的方法之一,通过使用map方法遍历当前的元素集合,返回结果是一个数组,数组的每一项则是元素的property属性。所以上面才通过join方法再次转成了字符串。

还有一点需要注意的是text方法设置或者获取都是在操作元素的textContent属性,那它和innerText,innerHTML的区别在哪呢?可以查看MDN

设置text

this.each(function (idx) {
  var newText = funcArg(this, text, idx, this.textContent)
  this.textContent = newText == null ? "" : "" + newText
})

设置与html的设置部分比较类似,既支持直接传入普通的字符串也支持传入回调函数。如果得到的newText为null或者undefined,会统一转成空字符串再进行设置。

val

获取或设置匹配元素的值。当没有给定value参数,返回第一个元素的值。如果是