這篇依然是跟 相關的方法,側重點是操作屬性的方法。 讀Zepto源碼系列文章已經放到了github上,歡迎star: "reading zepto" 源碼版本 本文閱讀的源碼為 "zepto1.2.0" 內部方法 setAttribute 如果屬性值 存在,則調用元素的原生方法 設置對應元素的指定屬 ...
這篇依然是跟 dom
相關的方法,側重點是操作屬性的方法。
讀Zepto源碼系列文章已經放到了github上,歡迎star: reading-zepto
源碼版本
本文閱讀的源碼為 zepto1.2.0
內部方法
setAttribute
function setAttribute(node, name, value) {
value == null ? node.removeAttribute(name) : node.setAttribute(name, value)
}
如果屬性值 value
存在,則調用元素的原生方法 setAttribute
設置對應元素的指定屬性值,否則調用 removeAttribute
刪除指定的屬性。
deserializeValue
// "true" => true
// "false" => false
// "null" => null
// "42" => 42
// "42.5" => 42.5
// "08" => "08"
// JSON => parse if valid
// String => self
function deserializeValue(value) {
try {
return value ?
value == "true" ||
(value == "false" ? false :
value == "null" ? null :
+value + "" == value ? +value :
/^[\[\{]/.test(value) ? $.parseJSON(value) :
value) :
value
} catch (e) {
return value
}
}
函數的主體又是個很複雜的三元表達式,但是函數要做什麼事情,註釋已經寫得很明白了。
try catch
保證出錯的情況下依然可以將原值返回。
先將這個複雜的三元表達式拆解下:
value ? 相當複雜的表達式返回的值 : value
值存在時,就進行相當複雜的三元表達式運算,否則返回原值。
再來看看 value === "true"
時的運算
value == "true" || (複雜表達式求出的值)
這其實是一個或操作,當 value === "true"
時就不執行後面的表達式,直接將 value === "true"
的值返回,也就是返回 true
再來看 value === false
時的求值
value == "false" ? false : (其他表達式求出來的值)
很明顯,value === "false"
時,返回的值為 false
value == "null" ? null : (其他表達式求出來的值)
為 value == "null"
時, 返回值為 null
再來看看數字字元串的判斷:
+value + "" == value ? +value : (其他表達式求出來的值)
這個判斷相當有意思。
+value
將 value
隱式轉換成數字類型,"42"
轉換成 42
,"08"
轉換成 8
,abc
會轉換成 NaN
。+ ""
是將轉換成數字後的值再轉換成字元串。然後再用 ==
和原值比較。這裡要註意,用的是 ==
,不是 ===
。左邊表達式不用說,肯定是字元串類型,右邊的如果為字元串類型,並且和左邊的值相等,那表示 value
為數字字元串,可以用 +value
直接轉換成數字。 但是以 0
開頭的數字字元串如 "08"
,經過左邊的轉換後變成 "8"
,兩個字元串不相等,繼續執行後面的邏輯。
如果 value
為數字,則左邊的字元串會再次轉換成數字後再和 value
進行比較,左邊轉換成數字後肯定為 value
本身,因此表達式成立,返回一樣的數字。
/^[\[\{]/.test(value) ? $.parseJSON(value) : value
這長長的三元表達式終於被剝得只剩下內衣了。
/^[\[\{]/
這個正則是檢測 value
是否以 [
或者 {
開頭,如果是,則將其作為對象或者數組,執行 $.parseJSON
方法反序列化,否則按原值返回。
其實,這個正則不太嚴謹的,以這兩個符號開頭的字元串,可能根本不是對象或者數組格式的,序列化可能會出錯,這就是一開始提到的 try catch
所負責的事了。
.html()
html: function(html) {
return 0 in arguments ?
this.each(function(idx) {
var originHtml = this.innerHTML
$(this).empty().append(funcArg(this, html, idx, originHtml))
}) :
(0 in this ? this[0].innerHTML : null)
},
html
方法既可以設置值,也可以獲取值,參數 html
既可以是固定值,也可以是函數。
html
方法的主體是一個三元表達式, 0 in arguments
用來判斷方法是否帶參數,如果不帶參數,則獲取值,否則,設置值。
(0 in this ? this[0].innerHTML : null)
先來看看獲取值,0 in this
是判斷集合是否為空,如果為空,則返回 null
,否則,返回的是集合第一個元素的 innerHTML
屬性值。
this.each(function(idx) {
var originHtml = this.innerHTML
$(this).empty().append(funcArg(this, html, idx, originHtml))
})
知道值怎樣獲取後,設置也就簡單了,要註意一點的是,設置值的時候,集合中每個元素的 innerHTML
值都被設置為給定的值。
由於參數 html
可以是固定值或者函數,所以先調用內部函數 funcArg
來對參數進行處理,funcArg
的分析請看 《讀Zepto源碼之樣式操作》 。
設置的邏輯也很簡單,先將當前元素的內容清空,調用的是 empty
方法,然後再調用 append
方法,插入給定的值到當前元素中。append
方法的分析請看《讀Zepto源碼之操作DOM》
.text()
text: function(text) {
return 0 in arguments ?
this.each(function(idx) {
var newText = funcArg(this, text, idx, this.textContent)
this.textContent = newText == null ? '' : '' + newText
}) :
(0 in this ? this.pluck('textContent').join("") : null)
},
text
方法用於獲取或設置元素的 textContent
屬性。
先看不傳參的情況:
(0 in this ? this.pluck('textContent').join("") : null)
調用 pluck
方法獲取每個元素的 textContent
屬性,並且將結果集合併成字元串。關於 textContent
和 innerText
的區別,MDN上說得很清楚:
textContent
會獲取所有元素的文本,包括script
和style
的元素innerText
不會將隱藏元素的文本返回innerText
元素遇到style
時,會重繪
具體參考 MDN:Node.textContent
設置值的邏輯中 html
方法差不多,但是在 newText == null
時,賦值為 ''
,否則,轉換成字元串。這個轉換我有點不太明白, 賦值給 textContent
時,會自動轉換成字元串,為什麼要自己轉換一次呢?還有,textContent
直接賦值為 null
或者 undefined
,也會自動轉換為 ''
,為什麼還要自己轉換一次呢?
.attr()
attr: function(name, value) {
var result
return (typeof name == 'string' && !(1 in arguments)) ?
(0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined) :
this.each(function(idx) {
if (this.nodeType !== 1) return
if (isObject(name))
for (key in name) setAttribute(this, key, name[key])
else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
})
},
attr
用於獲取或設置元素的屬性值。name
參數可以為 object
,用於設置多組屬性值。
判斷條件:
typeof name == 'string' && !(1 in arguments)
參數 name
為字元串,排除掉 name
為 object
的情況,並且第二個參數不存在,在這種情況下,為獲取值。
(0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined)
獲取屬性時,要滿足幾個條件:
- 集合不為空
- 集合的第一個元素的
nodeType
為ELEMENT_NODE
然後調用元素的原生方法 getAttribute
方法來獲取第一個元素對應的屬性值,如果屬性值 !=null
,則返回獲取到的屬性值,否則返回 undefined
。
再來看設置值的情況:
this.each(function(idx) {
if (this.nodeType !== 1) return
if (isObject(name))
for (key in name) setAttribute(this, key, name[key])
else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
})
如果元素的 nodeType
不為 ELEMENT_NODE
時,直接 return
當 name
為 object
時,遍歷對象,設置對應的屬性
否則,設置給定屬性的值。
.removeAttr()
removeAttr: function(name) {
return this.each(function() {
this.nodeType === 1 && name.split(' ').forEach(function(attribute) {
setAttribute(this, attribute)
}, this)
})
},
刪除給定的屬性。可以用空格分隔多個屬性。
調用的其實是 setAttribute
方法,只將元素和需要刪除的屬性傳遞進去, setAttribute
就會將對應的元素屬性刪除。
.prop()
propMap = {
'tabindex': 'tabIndex',
'readonly': 'readOnly',
'for': 'htmlFor',
'class': 'className',
'maxlength': 'maxLength',
'cellspacing': 'cellSpacing',
'cellpadding': 'cellPadding',
'rowspan': 'rowSpan',
'colspan': 'colSpan',
'usemap': 'useMap',
'frameborder': 'frameBorder',
'contenteditable': 'contentEditable'
}
prop: function(name, value) {
name = propMap[name] || name
return (1 in arguments) ?
this.each(function(idx) {
this[name] = funcArg(this, value, idx, this[name])
}) :
(this[0] && this[0][name])
},
prop
也是給元素設置或獲取屬性,但是跟 attr
不同的是, prop
設置的是元素本身固有的屬性,attr
用來設置自定義的屬性(也可以設置固有的屬性)。
propMap
是將一些特殊的屬性做一次映射。
prop
取值和設置值的時候,都是直接操作元素對象上的屬性,不需要調用如 setAttribute
的方法。
.removeProp()
removeProp: function(name) {
name = propMap[name] || name
return this.each(function() { delete this[name] })
},
刪除元素固定屬性,調用對象的 delete
方法就可以了。
.data()
capitalRE = /([A-Z])/g
data: function(name, value) {
var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase()
var data = (1 in arguments) ?
this.attr(attrName, value) :
this.attr(attrName)
return data !== null ? deserializeValue(data) : undefined
},
data
內部調用的是 attr
方法,但是給屬性名加上了 data-
首碼,這也是向規範靠攏。
name.replace(capitalRE, '-$1').toLowerCase()
稍微解釋下這個正則,capitalRE
匹配的是大寫字母,replace(capitalRE, '-$1')
是在大寫字母前面加上 -
連字元。這整個表達式其實就是將 name
轉換成 data-camel-case
的形式。
return data !== null ? deserializeValue(data) : undefined
如果 data
不嚴格為 null
時,調用 deserializeValue
序列化後返回,否則返回 undefined
。為什麼要用嚴格等 null
來作為判斷呢?這個我也不太明白,因為在獲取值時,attr
方法對不存在的屬性返回值為 undefined
,用 !== undefined
判斷會不會更好點呢?這樣 undefined
根本不需要再走 deserializeValue
方法。
.val()
val: function(value) {
if (0 in arguments) {
if (value == null) value = ""
return this.each(function(idx) {
this.value = funcArg(this, value, idx, this.value)
})
} else {
return this[0] && (this[0].multiple ?
$(this[0]).find('option').filter(function() { return this.selected }).pluck('value') :
this[0].value)
}
},
獲取或設置表單元素的 value
值。
如果傳參,還是慣常的套路,設置的是元素的 value
屬性。
否則,獲取值,看看獲取值的邏輯:
return this[0] && (this[0].multiple ?
$(this[0]).find('option').filter(function() { return this.selected }).pluck('value') :
this[0].value)
this[0].multiple
判斷是否為下拉列表多選,如果是,則找出所有選中的 option
,獲取選中的 option
的 value
值返回。這裡用到 pluck
方法來獲取屬性,具體的分析見:《讀Zepto源碼之集合元素查找》
否則,直接返回第一個元素的 value
值。
.offsetParent()
ootNodeRE = /^(?:body|html)$/i
offsetParent: function() {
return this.map(function() {
var parent = this.offsetParent || document.body
while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static")
parent = parent.offsetParent
return parent
})
}
查找最近的祖先定位元素,即最近的屬性 position
被設置為 relative
、absolute
和 fixed
的祖先元素。
var parent = this.offsetParent || document.body
獲取元素的 offsetParent
屬性,如果不存在,則預設賦值為 body
元素。
parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static"
判斷父級定位元素是否存在,並且不為根元素(即 body
元素或 html
元素),並且為相對定位元素,才進入迴圈,迴圈內是獲取下一個 offsetParent
元素。
這個應該做瀏覽器相容的吧,因為 offsetParent
本來返回的就是最近的定位元素。
.offset()
offset: function(coordinates) {
if (coordinates) return this.each(function(index) {
var $this = $(this),
coords = funcArg(this, coordinates, index, $this.offset()),
parentOffset = $this.offsetParent().offset(),
props = {
top: coords.top - parentOffset.top,
left: coords.left - parentOffset.left
}
if ($this.css('position') == 'static') props['position'] = 'relative'
$this.css(props)
})
if (!this.length) return null
if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
return { top: 0, left: 0 }
var obj = this[0].getBoundingClientRect()
return {
left: obj.left + window.pageXOffset,
top: obj.top + window.pageYOffset,
width: Math.round(obj.width),
height: Math.round(obj.height)
}
},
獲取或設置元素相對 document
的偏移量。
先來看獲取值:
if (!this.length) return null
if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
return { top: 0, left: 0 }
var obj = this[0].getBoundingClientRect()
return {
left: obj.left + window.pageXOffset,
top: obj.top + window.pageYOffset,
width: Math.round(obj.width),
height: Math.round(obj.height)
}
如果集合不存在,則返回 null
if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
return { top: 0, left: 0 }
如果集合中第一個元素不為 html
元素對象(document.documentElement !== this[0]
) ,並且不為 html
元素的子元素,則返回 { top: 0, left: 0 }
接下來,調用 getBoundingClientRect
,獲取元素的 width
和 height
值,以及相對視窗左上角的 left
和 top
值。具體參見文檔: Element.getBoundingClientRect()
因為 getBoundingClientRect
獲取到的位置是相對視窗的,因此需要將視窗外偏移量加上,即加上 window.pageXOffset
或 window.pageYOffset
。
再來看設置值:
if (coordinates) return this.each(function(index) {
var $this = $(this),
coords = funcArg(this, coordinates, index, $this.offset()),
parentOffset = $this.offsetParent().offset(),
props = {
top: coords.top - parentOffset.top,
left: coords.left - parentOffset.left
}
if ($this.css('position') == 'static') props['position'] = 'relative'
$this.css(props)
})
前面幾行都是固有的模式,不再展開,看看這段:
parentOffset = $this.offsetParent().offset()
獲取最近定位元素的 offset
值,這個值有什麼用呢?
props = {
top: coords.top - parentOffset.top,
left: coords.left - parentOffset.left
}
if ($this.css('position') == 'static') props['position'] = 'relative'
$this.css(props)
我們可以看到,設置偏移的時候,其實是設置元素的 left
和 top
值。如果父級元素有定位元素,那這個 left
和 top
值是相對於第一個父級定位元素的。
因此需要將傳入的 coords.top
和 coords.left
對應減掉第一個父級定位元素的 offset
的 top
和 left
值。
如果當前元素的 position
值為 static
,則將值設置為 relative
,相對自身偏移計算出來相差的 left
和 top
值。
.position()
position: function() {
if (!this.length) return
var elem = this[0],
offsetParent = this.offsetParent(),
offset = this.offset(),
parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()
offset.top -= parseFloat($(elem).css('margin-top')) || 0
offset.left -= parseFloat($(elem).css('margin-left')) || 0
parentOffset.top += parseFloat($(offsetParent[0]).css('border-top-width')) || 0
parentOffset.left += parseFloat($(offsetParent[0]).css('border-left-width')) || 0
return {
top: offset.top - parentOffset.top,
left: offset.left - parentOffset.left
}
},
返回相對父元素的偏移量。
offsetParent = this.offsetParent(),
offset = this.offset(),
parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()
分別獲取到第一個定位父元素 offsetParent
及相對文檔偏移量 parentOffset
,和自身的相對文檔偏移量 offset
。在獲取每一個定位父元素偏移量時,先判斷父元素是否為根元素,如果是,則 left
和 top
都返回 0
。
offset.top -= parseFloat($(elem).css('margin-top')) || 0
offset.left -= parseFloat($(elem).css('margin-left')) || 0
兩個元素之間的距離應該不包含元素的外邊距,因此將外邊距減去。
parentOffset.top += parseFloat($(offsetParent[0]).css('border-top-width')) || 0
parentOffset.left += parseFloat($(offsetParent[0]).css('border-left-width')) || 0
因為 position
返回的是距離第一個定位元素的 context box
的距離,因此父元素的 offset
的 left
和 top
值需要將 border
值加上(offset
算是的外邊距距離文檔的距離)。
return {
top: offset.top - parentOffset.top,
left: offset.left - parentOffset.left
}
最後,將他們距離文檔的偏移量相減就得到兩者間的偏移量了。
.scrollTop()
scrollTop: function(value) {
if (!this.length) return
var hasScrollTop = 'scrollTop' in this[0]
if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset
return this.each(hasScrollTop ?
function() { this.scrollTop = value } :
function() { this.scrollTo(this.scrollX, value) })
},
獲取或設置元素在縱軸上的滾動距離。
先看獲取值:
var hasScrollTop = 'scrollTop' in this[0]
if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset
如果存在 scrollTop
屬性,則直接用 scrollTop
獲取屬性,否則用 pageYOffset
獲取元素Y軸在屏幕外的距離,也即滾動高度了。
return this.each(hasScrollTop ?
function() { this.scrollTop = value } :
function() { this.scrollTo(this.scrollX, value) })
知道了獲取值後,設置值也簡單了,如果有 scrollTop
屬性,則直接設置這個屬性的值,否則調用 scrollTo
方法,用 scrollX
獲取到 x
軸的滾動距離,將 y
軸滾動到指定的距離 value
。
.scrollLeft()
scrollLeft: function(value) {
if (!this.length) return
var hasScrollLeft = 'scrollLeft' in this[0]
if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset
return this.each(hasScrollLeft ?
function() { this.scrollLeft = value } :
function() { this.scrollTo(value, this.scrollY) })
},
scrollLeft
原理同 scrollTop
,不再展開敘述。
系列文章
- 讀Zepto源碼之代碼結構
- 讀 Zepto 源碼之內部方法
- 讀Zepto源碼之工具函數
- 讀Zepto源碼之神奇的$
- 讀Zepto源碼之集合操作
- 讀Zepto源碼之集合元素查找
- 讀Zepto源碼之操作DOM
- 讀Zepto源碼之樣式操作
參考
License
作者:對角另一面