前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >读Zepto源码之样式操作

读Zepto源码之样式操作

作者头像
对角另一面
发布2017-12-27 14:06:49
2K0
发布2017-12-27 14:06:49
举报
文章被收录于专栏:对角另一面对角另一面

这篇依然是跟 dom 相关的方法,侧重点是操作样式的方法。

读Zepto源码系列文章已经放到了github上,欢迎star: reading-zepto

源码版本

本文阅读的源码为 zepto1.2.0

内部方法

classRE

classCache = {}

function classRE(name) {
  return name in classCache ?
    classCache[name] : (classCache[name] = new RegExp('(^|\\s)' + name + '(\\s|$)'))
}

这个函数是用来返回一个正则表达式,这个正则表达式是用来匹配元素的 class 名的,匹配的是如 className1 className2 className3 这样的字符串。

calssCache 初始化时是一个空对象,用 name 用为 key ,如果正则已经生成过,则直接从 classCache 中取出对应的正则表达式。

否则,生成一个正则表达式,存储到 classCache 中,并返回。

来看一下这个生成的正则,'(^|\\s)' 匹配的是开头或者空白(包括空格、换行、tab缩进等),然后连接指定的 name ,再紧跟着空白或者结束。

maybeAddPx

cssNumber = { 'column-count': 1, 'columns': 1, 'font-weight': 1, 'line-height': 1, 'opacity': 1, 'z-index': 1, 'zoom': 1 }

function maybeAddPx(name, value) {
  return (typeof value == "number" && !cssNumber[dasherize(name)]) ? value + "px" : value
}

在给属性设置值时,猜测所设置的属性可能需要带 px 单位时,自动给值拼接上单位。

cssNumber 是不需要设置 px 的属性值,所以这个函数里首先判断设置的值是否为 number 类型,如果是,并且需要设置的属性不在 cssNumber 中时,给值拼接上 px 单位。

defaultDisplay

elementDisplay = {}

function defaultDisplay(nodeName) {
  var element, display
  if (!elementDisplay[nodeName]) {
    element = document.createElement(nodeName)
    document.body.appendChild(element)
    display = getComputedStyle(element, '').getPropertyValue("display")
    element.parentNode.removeChild(element)
    display == "none" && (display = "block")
    elementDisplay[nodeName] = display
  }
  return elementDisplay[nodeName]
}

先透露一下,这个方法是给 .show() 用的,show 方法需要将元素显示出来,但是要显示的时候能不能直接将 display 设置成 block 呢?显然是不行的,来看一下 display 的可能会有那些值:

display: none

display: inline
display: block
display: contents
display: list-item
display: inline-block
display: inline-table
display: table
display: table-cell
display: table-column
display: table-column-group
display: table-footer-group
display: table-header-group
display: table-row
display: table-row-group
display: flex
display: inline-flex
display: grid
display: inline-grid
display: ruby
display: ruby-base
display: ruby-text
display: ruby-base-container
display: ruby-text-container 
display: run-in

display: inherit
display: initial
display: unset

如果元素原来的 display 值为 table ,调用 show 后变成 block 了,那页面的结构可能就乱了。

这个方法就是将元素显示时默认的 display 值缓存到 elementDisplay,并返回。

函数用节点名 nodeNamekey ,如果该节点显示时的 display 值已经存在,则直接返回。

element = document.createElement(nodeName)
document.body.appendChild(element)

否则,使用节点名创建一个空元素,并且将元素插入到页面中

display = getComputedStyle(element, '').getPropertyValue("display")
element.parentNode.removeChild(element)

调用 getComputedStyle 方法,获取到元素显示时的 display 值。获取到值后将所创建的元素删除。

display == "none" && (display = "block")
elementDisplay[nodeName] = display

如果获取到的 display 值为 none ,则将显示时元素的 display 值默认为 block。然后将结果缓存起来。display 的默认值为 none? Are you kiding me ? 真的有这种元素吗?还真的有,像 styleheadtitle 等元素的默认值都是 none 。将 styleheaddisplay 设置为 block ,并且将 stylecontenteditable 属性设置为 truestyle 就显示出来了,直接在页面上一边敲样式,一边看效果,爽!!!

关于元素的 display 默认值,可以看看这篇文章 Default CSS Display Values for Different HTML Elements

funcArg

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

这个函数要注意,本篇和下一篇介绍的绝大多数方法都会用到这个函数。

例如本篇将要说到的 addClassremoveClass 等方法的参数可以为固定值或者函数,这些方法的参数即为形参 arg

当参数 arg 为函数时,调用 argcall 方法,将上下文 context ,当前元素的索引 idx 和原始值 payload 作为参数传递进去,将调用结果返回。

如果为固定值,直接返回 arg

className

function className(node, value) {
  var klass = node.className || '',
      svg = klass && klass.baseVal !== undefined

  if (value === undefined) return svg ? klass.baseVal : klass
  svg ? (klass.baseVal = value) : (node.className = value)
}

className 包含两个参数,为元素节点 node 和需要设置的样式名 value

如果 value 不为 undefined(可以为空,注意判断条件为 value === undefined,用了全等判断),则将元素的 className 设置为给定的值,否则将元素的 className 值返回。

这个函数对 svg 的元素做了兼容,如果元素的 className 属性存在,并且 className 属性存在 baseVal 时,为 svg 元素,如果是 svg 元素,取值和赋值都是通过 baseVal 。对 svg 不是很熟,具体见文档: SVGAnimatedString.baseVal

.css()

css: function(property, value) {
  if (arguments.length < 2) {
    var element = this[0]
    if (typeof property == 'string') {
      if (!element) return
      return element.style[camelize(property)] || getComputedStyle(element, '').getPropertyValue(property)
        } else if (isArray(property)) {
          if (!element) return
          var props = {}
          var computedStyle = getComputedStyle(element, '')
          $.each(property, function(_, prop) {
            props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop))
          })
          return props
        }
  }

  var css = ''
  if (type(property) == 'string') {
    if (!value && value !== 0)
      this.each(function() { this.style.removeProperty(dasherize(property)) })
      else
        css = dasherize(property) + ":" + maybeAddPx(property, value)
        } else {
          for (key in property)
            if (!property[key] && property[key] !== 0)
              this.each(function() { this.style.removeProperty(dasherize(key)) })
              else
                css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'
                }

  return this.each(function() { this.style.cssText += ';' + css })
}

css 方法有两个参数,property 是的 css 样式名,value 是需要设置的值,如果不传递 value 值则为取值操作,否则为赋值操作。

来看看调用方式:

css(property)   ⇒ value  // 获取值
css([property1, property2, ...])   ⇒ object // 获取值
css(property, value)   ⇒ self // 设置值
css({ property: value, property2: value2, ... })   ⇒ self // 设置值

下面这段便是处理获取值情况的代码:

if (arguments.length < 2) {
  var element = this[0]
  if (typeof property == 'string') {
    if (!element) return
    return element.style[camelize(property)] || getComputedStyle(element, '').getPropertyValue(property)
      } else if (isArray(property)) {
        if (!element) return
        var props = {}
        var computedStyle = getComputedStyle(element, '')
        $.each(property, function(_, prop) {
          props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop))
        })
        return props
      }
}

当为获取值时,css 方法必定只传递了一个参数,所以用 arguments.length < 2 来判断,用 css 方法来获取值,获取的是集合中第一个元素对应的样式值。

if (!element) return
return element.style[camelize(property)] || getComputedStyle(element, '').getPropertyValue(property)

propertystring 时,如果元素不存在,直接 return 掉。

如果 style 中存在对应的样式值,则优先获取 style 中的样式值,否则用 getComputedStyle 获取计算后的样式值。

为什么不直接获取计算后的样式值呢?因为用 style 获取的样式值是原始的字符串,而 getComputedStyle 顾名思义获取到的是计算后的样式值,如 style = "transform: translate(10px, 10px)"style.transform 获取到的值为 translate(10px, 10px),而用 getComputedStyle 获取到的是 matrix(1, 0, 0, 1, 10, 10)。这里用到的 camelize 方法是将属性 property 转换成驼峰式的写法,该方法在《读Zepto源码之内部方法》有过分析。

else if (isArray(property)) {
  if (!element) return
  var props = {}
  var computedStyle = getComputedStyle(element, '')
  $.each(property, function(_, prop) {
    props[prop] = (element.style[camelize(prop)] || computedStyle.getPropertyValue(prop))
  })
  return props
}

如果参数 property 为数组时,表示要获取一组属性的值。isArray 方法也在《读Zepto源码之内部方法》有过分析。

获取的方法也很简单,遍历 property ,获取 style 上对应的样式值,如果 style 上的值不存在,则通过 getComputedStyle 来获取,返回的是以样式名为 keyvalue 为对应的样式值的对象。

接下来是给所有元素设置值的情况:

var css = ''
if (type(property) == 'string') {
  if (!value && value !== 0)
    this.each(function() { this.style.removeProperty(dasherize(property)) })
  else
    css = dasherize(property) + ":" + maybeAddPx(property, value)
 } else {
    for (key in property)
        if (!property[key] && property[key] !== 0)
            this.each(function() { this.style.removeProperty(dasherize(key)) })
         else
            css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'
         }

return this.each(function() { this.style.cssText += ';' + css })

这里定义了个变量 css 来接收需要新值的样式字符串。

if (type(property) == 'string') {
  if (!value && value !== 0)
    this.each(function() { this.style.removeProperty(dasherize(property)) })
  else
    css = dasherize(property) + ":" + maybeAddPx(property, value)
 }

当参数 property 为字符串时

如果 value 不存在并且值不为 0 时(注意,valueundefined 时,已经在上面处理过了,也即是获取样式值),遍历集合,将对应的样式值从 style 中删除。

否则,拼接样式字符串,拼接成如 width:100px 形式的字符串。这里调用了 maybeAddPx 的方法,自动给需要加 px 的属性值拼接上了 px 单位。this.css('width', 100)this.css('width', '100px') 会得到一样的结果。

for (key in property)
  if (!property[key] && property[key] !== 0)
    this.each(function() { this.style.removeProperty(dasherize(key)) })
    else
      css += dasherize(key) + ':' + maybeAddPx(key, property[key]) + ';'

propertykey 是样式名,value 为样式值的对象时,用 for...in 遍历对象,接下来的处理逻辑跟 propertystring 时差不多,在做 css 拼接时,在末尾加了 ;,避免遍历时,将样式名和值连接在了一起。

.hide()

hide: function() {
  return this.css("display", "none")
},

将集合中所有元素的 display 样式属性设置为 node,就达到了隐藏元素的目的。注意,css 方法中已经包含了 each 循环。

.show()

show: function() {
  return this.each(function() {
    this.style.display == "none" && (this.style.display = '')
    if (getComputedStyle(this, '').getPropertyValue("display") == "none")
      this.style.display = defaultDisplay(this.nodeName)
      })
},

hide 方法是直接将 display 设置为 none 即可,show 可不可以直接将需要显示的元素的 display 设置为 block 呢?

这样在大多数情况下是可以的,但是碰到像 tableli 等显示时 display 默认值不是 block 的元素,强硬将它们的 display 属性设置为 block ,可能会更改他们的默认行为。

show 要让元素真正显示,要经过两步检测:

this.style.display == "none" && (this.style.display = '')

如果 style 中的 display 属性为 none ,先将 style 中的 display 置为 ``。

if (getComputedStyle(this, '').getPropertyValue("display") == "none")
  this.style.display = defaultDisplay(this.nodeName)
 })

这样还未完,内联样式的 display 属性是置为空了,但是如果嵌入样式或者外部样式表中设置了 displaynone 的样式,或者本身的 display 默认值就是 none 的元素依然显示不了。所以还需要用获取元素的计算样式,如果为 none ,则将 display 的属性设置为元素显示时的默认值。如 table 元素的 style 中的 display 属性值会被设置为 table

.toggle()

toggle: function(setting) {
  return this.each(function() {
    var el = $(this);
    (setting === undefined ? el.css("display") == "none" : setting) ? el.show(): el.hide()
  })
},

切换元素的显示和隐藏状态,如果元素隐藏,则显示元素,如果元素显示,则隐藏元素。可以用参数 setting 指定 toggle 的行为,如果指定为 true ,则显示,如果为 falsesetting 不一定为 Boolean),则隐藏。

注意,判断条件是 setting === undefined ,用了全等,只有在不传参,或者传参为 undefined 的时候,条件才会成立。

.hasClass()

hasClass: function(name) {
  if (!name) return false
  return emptyArray.some.call(this, function(el) {
    return this.test(className(el))
  }, classRE(name))
},

判断集合中的元素是否存在指定 nameclass 名。

如果没有指定 name 参数,则直接返回 false

否则,调用 classRE 方法,生成检测样式名的正则,传入数组方法 some,要注意, some 里面的 this 值并不是遍历的当前元素,而是传进去的 classRE(name) 正则,回调函数中的 el 才是当前元素。具体参考文档 Array.prototype.some()

调用 className 方法,获取当前元素的 className 值,如果有一个元素匹配了正则,则返回 true

.addClass()

addClass: function(name) {
  if (!name) return this
  return this.each(function(idx) {
    if (!('className' in this)) return
    classList = []
    var cls = className(this),
        newName = funcArg(this, name, idx, cls)
    newName.split(/\s+/g).forEach(function(klass) {
      if (!$(this).hasClass(klass)) classList.push(klass)
        }, this)
    classList.length && className(this, cls + (cls ? " " : "") + classList.join(" "))
  })
},

为集合中的所有元素增加指定类名 namename 可以为固定值或者函数。

如果 name 没有传递,则返回当前集合 this ,以进行链式操作。

如果 name 存在,遍历集合,判断当前元素是否存在 className 属性,如果不存在,立即退出循环。要注意,在 each 遍历中,this 指向的是当前元素。

classList = []
var cls = className(this),
    newName = funcArg(this, name, idx, cls)

classList 用来接收需要增加的样式类数组。不太明白为什么要用全局变量 classList 来接收,用局部变量不是更好点吗?

cls 保存当前类的字符串,使用函数 className 获得。

newName 是需要新增的样式类字符串,因为 name 可以是函数或固定值,统一交由 funcArg 来处理。

newName.split(/\s+/g).forEach(function(klass) {
  if (!$(this).hasClass(klass)) classList.push(klass)
    }, this)
classList.length && className(this, cls + (cls ? " " : "") + classList.join(" "))

newName.split(/\s+/g) 是将 newName 字符串,用空白分割成数组。

再对数组遍历,得到单个类名,调用 hasClass 判断类名是否已经存在于元素的 className 中,如果不存在,将类名 push 进数组 classList 中。

如果 classList 不为空,则调用 className 方法给元素设置值。classList.join(" ") 是将类名转换成用空格分隔的字符串,如果 cls 即元素原来就存在有其他类名,拼接时也使用空格分隔开。

.removeClass()

removeClass: function(name) {
  return this.each(function(idx) {
    if (!('className' in this)) return
    if (name === undefined) return className(this, '')
    classList = className(this)
    funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass) {
      classList = classList.replace(classRE(klass), " ")
    })
    className(this, classList.trim())
  })
},

删除元素中指定的类 name 。如果不传递参数,则将 className 属性置为空,也即删除所有样式类。

classList = className(this)
funcArg(this, name, idx, classList).split(/\s+/g).forEach(function(klass) {
  classList = classList.replace(classRE(klass), " ")
})
className(this, classList.trim())

这是的 classList 依然是全局变量,但是接收的是当前元素的当前样式类字符串(为什么不用局部变量呢?)。

参数 name 依然可以为函数或者固定值,因此用 funcArg 来处理,然后用空白分割成数组,再遍历得到单个样式类,调用 replace 方法,如果 classList 中能匹配到这个类,则将匹配的字符串替换成空格,这样就达到了删除的目的。

最后,用 trimclassList 的头尾空格去掉,调用 className 方法,重新给当前元素的 className 赋值。

.toggleClass()

toggleClass: function(name, when) {
  if (!name) return this
  return this.each(function(idx) {
    var $this = $(this),
        names = funcArg(this, name, idx, className(this))
    names.split(/\s+/g).forEach(function(klass) {
      (when === undefined ? !$this.hasClass(klass) : when) ?
        $this.addClass(klass): $this.removeClass(klass)
    })
  })
},

切换样式类,如果样式类不存在,则增加样式类,如果存在,则删除样式类。

toggleClass 接收两个参数,name 是需要切换的类名, when 是指定切换的方法,如果 whentrue ,则增加样式类,为 false ,则删除样式类。when 不一定要为 Boolean 类型。

这个方法跟 toggle 方法的逻辑参不多,只不过调用的方法变成 addClassremoveClass ,可以参考 toggle 的实现,不用过多分析。

系列文章

  1. 读Zepto源码之代码结构
  2. 读 Zepto 源码之内部方法
  3. 读Zepto源码之工具函数
  4. 读Zepto源码之神奇的$
  5. 读Zepto源码之集合操作
  6. 读Zepto源码之集合元素查找
  7. 读Zepto源码之操作DOM

参考

License

License: CC BY-NC-ND 4.0
License: CC BY-NC-ND 4.0

作者:对角另一面

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2017-06-11 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 源码版本
  • 内部方法
    • classRE
      • maybeAddPx
        • defaultDisplay
          • funcArg
            • className
            • .css()
            • .hide()
            • .show()
            • .toggle()
            • .hasClass()
            • .addClass()
            • .removeClass()
            • .toggleClass()
            • 系列文章
            • 参考
            • License
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档