读 Zepto 源码之款式操纵

这篇依旧是跟 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

《读 Zepto 源码之款式操纵》

末了,一切文章都邑同步发送到微信民众号上,迎接关注,迎接提意见: 《读 Zepto 源码之款式操纵》

作者:对角另一面

    原文作者:对角另一面
    原文地址: https://segmentfault.com/a/1190000009735935
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞