Zepto这样操作元素属性

前言

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

原文链接

源码仓库

1-YjgiXiu84rNLHrLCZok-kg.jpg

attr()

  1. 读取或设置dom的属性。
  2. 如果没有给定value参数,则读取对象集合中第一个元素的属性值。
  3. 当给定了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参数传入的是否是对象进行两个分支的操作。

  1. 如果name是个对象,那对对象进行遍历,再挨个调用setAttribute方法,进行�属性设置操作。
  2. �不是对象的话,接下来的这行代码,让第二个参数既可以传入普通的字符串,也可以传入回调函数。

看个实际使用�的例子

$('.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

  1. text() ⇒ string
  2. text(content) ⇒ self
  3. 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参数,返回第一个元素的值。如果是<select multiple>标签,则返回一个数组。当给定value参数,那么将设置所有元素的值。val

  1. val() ⇒ string
  2. val(value) ⇒ self
  3. val(function(index, oldValue){ ... }) ⇒ self

以上是基本用法

源码分析

val: function (value) {
  if (0 in arguments) {
    if (value == null) value = ""
    return this.each(function (idx) {
      this.value = funcArg(this, value, idx, this.value)
    })
  } else {
    return this[0] && (this[0].multiple ?
      $(this[0]).find('option').filter(function () { return this.selected }).pluck('value') :
      this[0].value)
  }
}

html,textval方法对待取值和设置值的套路基本都是一样的,判断有没有传入第一个参数,有则认为是设置,没有就是读取。

先看读取部分


return this[0] && (this[0].multiple ?
  $(this[0]).find('option').filter(function () { return this.selected }).pluck('value') :
  this[0].value)


假设this[0](也就是元素集合中第一个元素存在)我们把它拆成两个部分来学习

  1. 获取多选下拉列表的value
  2. 普通表单元素value

this[0].multiple ? '获取多选下拉列表的value' : '普通表单元素value'

针对第一种情况首先会通过find函数取查找子元素option集合,然后再过this.selected过滤出已经选中的option元素数组,最后还是通过调用pluck函数返回该option元素集合中的value数组。

第二种情况则是直接读取元素的value属性即可。

接下来我们回去继续看设置部分

if (value == null) value = ""
  return this.each(function (idx) {
    this.value = funcArg(this, value, idx, this.value)
  })

与html,text等方法类似,通过调用funcArg方法使得既支持普通字符串设置,也支持传入回调函数返回值设置值。

data

读取或写入dom的 data-* 属性。行为有点像 attr ,但是属性名称前面加上 data-。#data

  1. data(name) ⇒ value
  2. data(name, value) ⇒ self

注意:data方法本质上也是借用attr方法去实现的,不同之处在于data设置或者读取的属性为data-打头。

源码分析


data: function (name, value) {
  var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase()

  var data = (1 in arguments) ?
    this.attr(attrName, value) :
    this.attr(attrName)

  return data !== null ? deserializeValue(data) : undefined
},


data方法源码�分为三个部分

  1. 将传入的name属性转化为data-开头的连字符
  2. 通过attr方法设置或者获取属性
  3. 对attr方法的返回值再做一层映射处理

我们分别一一解释一下这几个部分

var capitalRE = /([A-Z])/g
var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase()

将小驼峰书写形式转换成以data-开头的连字符形式�,例如zeptoAnalysis => data-zepto-analysis

第二部分调用attr方法去设置�后者获取元素的属性

第三部分挺有意思的,读取属性值时,会有下列转换:

  1. “true”, “false”, and “null” 被转换为相应的类型;
  2. 数字值转换为实际的数字类型;
  3. JSON值将会被解析,如果它是有效的JSON;
  4. 其它的一切作为字符串返回。

来看一下这些转转操作是如何通过deserializeValue方法完成的。


function deserializeValue(value) {
  try {
    return value ?
      value == "true" ||
      (value == "false" ? false :
        value == "null" ? null :
          +value + "" == value ? +value :
            /^[\[\{]/.test(value) ? $.parseJSON(value) :
              value)
      : value
  } catch (e) {
    return value
  }
}

这个函数用的三元表达式比较复杂,一步步解析如下

  1. 如果value存在,则进行第2步,否则直接返回value
  2. 当value为字符串”true“时,返回true,否则进行第3步
  3. 当value为字符串“false”时,返回false,否则进行第4步
  4. 当value为字符串“null”时,返回null,否则进行第5步
  5. 当value为类似“12”这种类型字符串时,返回12(注意:+'12' => 12, +'01' => 1),否则进行第6步
  6. 当value以{或者[为开头时,使用parseJSON解析(但是有点不严格,因为以{[开头不一定就是对象字符串),否则直接返回value

最后还有一个问题,不知道大家有没有注意到zepto模块中的data方法和data模块中的data方法都是挂载到原型下面的,那他们之间到底有什么关系呢?可以查看之前写的一篇文章Zepto中数据缓存原理与实现 ,应该可以找到答案

结尾

以上是Zepto中常见的操作元素属性的方法(attr、removeAttr、prop、removeProp、html、text、val、data)解析。欢迎指正其中的问题。

参考

  1. 读Zepto源码之属性操作

  2. textContent mdn

  3. multiple

  4. zepto.js 源码解析

文章记录

ie模块

  1. Zepto源码分析之ie模块(2017-11-03)

data模块

  1. Zepto中数据缓存原理与实现(2017-10-03)

form模块

  1. zepto源码分析之form模块(2017-10-01)

zepto模块

  1. 这些Zepto中实用的方法集(2017-08-26)
  2. Zepto核心模块之工具方法拾遗 (2017-08-30)
  3. 看zepto如何实现增删改查DOM (2017-10-2)
  4. Zepto这样操作元素属性(2017-11-13)

event模块

  1. mouseenter与mouseover为何这般纠缠不清?(2017-06-05)
  2. 向zepto.js学习如何手动触发DOM事件(2017-06-07)
  3. 谁说你只是"会用"jQuery?(2017-06-08)

ajax模块

  1. 原来你是这样的jsonp(原理与具体实现细节)(2017-06-11)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,517评论 18 139
  • 1.JQuery 基础 改变web开发人员创造搞交互性界面的方式。设计者无需花费时间纠缠JS复杂的高级特性。 1....
    LaBaby_阅读 1,316评论 0 2
  • 1.JQuery 基础 改变web开发人员创造搞交互性界面的方式。设计者无需花费时间纠缠JS复杂的高级特性。 1....
    LaBaby_阅读 1,150评论 0 1
  • 1、首先是iOS和安卓的渲染机制不同,iOS的UI渲染采用的是实时优先级,而安卓的UI渲染采用的是遵循pc模式的主...
    Mrxiaowang阅读 476评论 0 1
  • 生活中许多情感都败在距离上,远了生出不满,近了又生出矛盾,距离其实是彼此的一种尊重。在爱中需要距离,没有距离相处是...
    陳禾柬阅读 438评论 0 0