1. 程式人生 > >讀Zepto源碼之樣式操作

讀Zepto源碼之樣式操作

blob repl dst sel ddc itl put append github上

這篇依然是跟 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

參考

  • MDN: display
  • Default CSS Display Values for Different HTML Elements
  • SVGAnimatedString.baseVal
  • 獲取元素CSS值之getComputedStyle方法熟悉
  • Array.prototype.some()

License

作者:對角另一面

讀Zepto源碼之樣式操作