讀Zepto源碼之樣式操做

這篇依然是跟 dom 相關的方法,側重點是操做樣式的方法。javascript

讀Zepto源碼系列文章已經放到了github上,歡迎star: reading-zeptocss

源碼版本

本文閱讀的源碼爲 zepto1.2.0html

內部方法

classRE

classCache = {}

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

這個函數是用來返回一個正則表達式,這個正則表達式是用來匹配元素的 class 名的,匹配的是如 className1 className2 className3 這樣的字符串。java

calssCache 初始化時是一個空對象,用 name 用爲 key ,若是正則已經生成過,則直接從 classCache 中取出對應的正則表達式。node

不然,生成一個正則表達式,存儲到 classCache 中,並返回。git

來看一下這個生成的正則,'(^|\\s)' 匹配的是開頭或者空白(包括空格、換行、tab縮進等),而後鏈接指定的 name ,再緊跟着空白或者結束。github

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 單位時,自動給值拼接上單位。web

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

做者:對角另外一面

相關文章
相關標籤/搜索