讀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
,並返回。
函數用節點名 nodeName
為 key
,如果該節點顯示時的 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 ? 真的有這種元素嗎?還真的有,像 style
、 head
和 title
等元素的默認值都是 none
。將 style
和 head
的 display
設置為 block
,並且將 style
的 contenteditable
屬性設置為 true
,style
就顯示出來了,直接在頁面上一邊敲樣式,一邊看效果,爽!!!
關於元素的 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
}
這個函數要註意,本篇和下一篇介紹的絕大多數方法都會用到這個函數。
例如本篇將要說到的 addClass
和 removeClass
等方法的參數可以為固定值或者函數,這些方法的參數即為形參 arg
。
當參數 arg
為函數時,調用 arg
的 call
方法,將上下文 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)
當 property
為 string
時,如果元素不存在,直接 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
來獲取,返回的是以樣式名為 key
,value
為對應的樣式值的對象。
接下來是給所有元素設置值的情況:
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
時(註意,value
為 undefined
時,已經在上面處理過了,也即是獲取樣式值),遍歷集合,將對應的樣式值從 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]) + ‘;‘
當 property
為 key
是樣式名,value
為樣式值的對象時,用 for...in
遍歷對象,接下來的處理邏輯跟 property
為 string
時差不多,在做 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
呢?
這樣在大多數情況下是可以的,但是碰到像 table
、li
等顯示時 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
屬性是置為空了,但是如果嵌入樣式或者外部樣式表中設置了 display
為 none
的樣式,或者本身的 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
,則顯示,如果為 false
( setting
不一定為 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))
},
判斷集合中的元素是否存在指定 name
的 class
名。
如果沒有指定 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(" "))
})
},
為集合中的所有元素增加指定類名 name
。 name
可以為固定值或者函數。
如果 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
中能匹配到這個類,則將匹配的字符串替換成空格,這樣就達到了刪除的目的。
最後,用 trim
將 classList
的頭尾空格去掉,調用 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
是指定切換的方法,如果 when
為 true
,則增加樣式類,為 false
,則刪除樣式類。when
不一定要為 Boolean
類型。
這個方法跟 toggle
方法的邏輯參不多,只不過調用的方法變成 addClass
和 removeClass
,可以參考 toggle
的實現,不用過多分析。
系列文章
- 讀Zepto源碼之代碼結構
- 讀 Zepto 源碼之內部方法
- 讀Zepto源碼之工具函數
- 讀Zepto源碼之神奇的$
- 讀Zepto源碼之集合操作
- 讀Zepto源碼之集合元素查找
- 讀Zepto源碼之操作DOM
參考
- MDN: display
- Default CSS Display Values for Different HTML Elements
- SVGAnimatedString.baseVal
- 獲取元素CSS值之getComputedStyle方法熟悉
- Array.prototype.some()
License
作者:對角另一面
讀Zepto源碼之樣式操作