摘要:為什么要用嚴格等來作為判斷呢這個我也不太明白,因為在獲取值時,方法對不存在的屬性返回值為,用判斷會不會更好點呢這樣根本不需要再走方法。
這篇依然是跟 dom 相關的方法,側重點是操作屬性的方法。
讀Zepto源碼系列文章已經放到了github上,歡迎star: reading-zepto
源碼版本本文閱讀的源碼為 zepto1.2.0
內部方法 setAttributefunction 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 ,不再展開敘述。
width/height 方法生成器["width", "height"].forEach(function(dimension) { var dimensionProperty = dimension.replace(/./, function(m) { return m[0].toUpperCase() }) $.fn[dimension] = function(value) { var offset, el = this[0] if (value === undefined) return isWindow(el) ? el["inner" + dimensionProperty] : isDocument(el) ? el.documentElement["scroll" + dimensionProperty] : (offset = this.offset()) && offset[dimension] else return this.each(function(idx) { el = $(this) el.css(dimension, funcArg(this, value, idx, el[dimension]())) }) } })
這個函數用來生成 width 和 height 方法。
var dimensionProperty = dimension.replace(/./, function(m) { return m[0].toUpperCase() })
這段將 width 和 height 的首字母變成大寫,即 Width 和 Height 的形式。
width 和 height 同樣有獲取值和設置值的功能,先來看獲取值的實現。
isWindow(el) ? el["inner" + dimensionProperty] : isDocument(el) ? el.documentElement["scroll" + dimensionProperty] : (offset = this.offset()) && offset[dimension]
如果第一個元素為 window 對象,則用 innerWidth 或 innerHeight 獲取值。window 關于寬高的屬性有 innerWidth 、 innerHeight 和 outerWidth 、outerHeight ,inner 相關的屬性是獲取瀏覽器視窗的寬度或者高度,而 outer 相關的屬性獲取是的整個瀏覽器的寬度或高度,包含標簽欄,地址欄等。
如果是 document 對象,則用 el.documentElement.scrollWidth 或 el.documentElement.scrollHeight 來獲取值。document.documentElement 對象上有 scrollWidth/scrollHeight 、 offsetWidth/offsetHeight 和 clientWidth/clientHeight 等寬高相關的屬性,三者有什么區別呢,可以看看這篇文章:《web前端學習筆記---scrollWidth,clientWidth,offsetWidth的區別》
對于普通元素,獲取值就簡單了,調用的是上面說到過的 offset 方法,就能取到對應的 width 和 height 的值了。
this.each(function(idx) { el = $(this) el.css(dimension, funcArg(this, value, idx, el[dimension]())) })
設置值就相對簡單了,調用的是 css 方法,直接設置對應的屬性就可以了。關于 css 方法的實現,可以看看上篇:《讀Zepto源碼之樣式操作》
系列文章讀Zepto源碼之代碼結構
讀 Zepto 源碼之內部方法
讀Zepto源碼之工具函數
讀Zepto源碼之神奇的$
讀Zepto源碼之集合操作
讀Zepto源碼之集合元素查找
讀Zepto源碼之操作DOM
讀Zepto源碼之樣式操作
參考MDN:Node.textContent
data-*
Element.getBoundingClientRect()
window
web前端學習筆記---scrollWidth,clientWidth,offsetWidth的區別
?
License最后,所有文章都會同步發送到微信公眾號上,歡迎關注,歡迎提意見:
作者:對角另一面
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83653.html
摘要:的模塊用來獲取節點中的屬性的數據,和儲存跟相關的數據。獲取節點指定的緩存值。如果存在,則刪除指定的數據,否則將緩存的數據全部刪除。為所有下級節點,如果為方法,則節點自身也是要被移除的,所以需要將自身也加入到節點中。 Zepto 的 Data 模塊用來獲取 DOM 節點中的 data-* 屬性的數據,和儲存跟 DOM 相關的數據。 讀 Zepto 源碼系列文章已經放到了github上,歡...
摘要:所以模塊依賴于模塊,在引入前必須引入模塊。原有的方法分析見讀源碼之樣式操作方法首先調用原有的方法,將元素顯示出來,這是實現動畫的基本條件。如果沒有傳遞,或者為值,則表示不需要動畫,調用原有的方法即可。 fx 模塊提供了 animate 動畫方法,fx_methods 利用 animate 方法,提供一些常用的動畫方法。所以 fx_methods 模塊依賴于 fx 模塊,在引入 fx_m...
摘要:讀源碼系列文章已經放到了上,歡迎源碼版本本文閱讀的源碼為改寫原有的方法模塊改寫了以上這些方法,這些方法在調用的時候,會為返回的結果添加的屬性,用來保存原來的集合。方法的分析可以看讀源碼之模塊。 Stack 模塊為 Zepto 添加了 addSelf 和 end 方法。 讀 Zepto 源碼系列文章已經放到了github上,歡迎star: reading-zepto 源碼版本 本文閱讀的...
摘要:模塊處理的是表單提交。表單提交包含兩部分,一部分是格式化表單數據,另一部分是觸發事件,提交表單。最終返回的結果是一個數組,每個數組項為包含和屬性的對象。否則手動綁定事件,如果沒有阻止瀏覽器的默認事件,則在第一個表單上觸發,提交表單。 Form 模塊處理的是表單提交。表單提交包含兩部分,一部分是格式化表單數據,另一部分是觸發 submit 事件,提交表單。 讀 Zepto 源碼系列文章已...
摘要:模塊基于上的事件的封裝,利用屬性,封裝出系列事件。這個判斷需要引入設備偵測模塊。然后是監測事件,根據這三個事件,可以組合出和事件。其中變量對象和模塊中的對象的作用差不多,可以先看看讀源碼之模塊對模塊的分析。 Gesture 模塊基于 IOS 上的 Gesture 事件的封裝,利用 scale 屬性,封裝出 pinch 系列事件。 讀 Zepto 源碼系列文章已經放到了github上,歡...
摘要:模塊是為解決移動版加載圖片過大過多時崩潰的問題。因為沒有處理過這樣的場景,所以這部分的代碼解釋不會太多,為了說明這個問題,我翻譯了這篇文章作為附文怎樣處理移動端對圖片資源的限制,更詳細地解釋了這個模塊的應用場景。 assets 模塊是為解決 Safari 移動版加載圖片過大過多時崩潰的問題。因為沒有處理過這樣的場景,所以這部分的代碼解釋不會太多,為了說明這個問題,我翻譯了《How to...
閱讀 3398·2021-10-11 11:06
閱讀 2182·2019-08-29 11:10
閱讀 1944·2019-08-26 18:18
閱讀 3255·2019-08-26 13:34
閱讀 1559·2019-08-23 16:45
閱讀 1037·2019-08-23 16:29
閱讀 2797·2019-08-23 13:11
閱讀 3226·2019-08-23 12:58