這篇依然是跟 dom
相關的方法,側重點是操作 dom
的方法。
讀Zepto源碼系列文章已經放到了github上,歡迎star: reading-zepto
源碼版本
本文閱讀的源碼爲 zepto1.2.0
.remove()
remove: function() {
return this.each(function() {
if (this.parentNode != null)
this.parentNode.removeChild(this)
})
},
刪除當前集合中的元素。
如果父節點存在時,則用父節點的 removeChild
方法來刪掉當前的元素。
相似方法生成器
zepto
中 after
、 prepend
、 before
、 append
、insertAfter
、 insertBefore
、 appendTo
和 prependTo
都是通過這個相似方法生成器生成的。
定義容器
adjacencyOperators = ['after', 'prepend', 'before', 'append']
首先,定義了一個相似操作的數組,注意數組裏面只有 after
、 prepend
、 before
、 append
這幾個方法名,後面會看到,在生成這幾個方法後,insertAfter
、 insertBefore
、 appendTo
和 prependTo
會分別調用前面生成的幾個方法。
輔助方法traverseNode
function traverseNode(node, fun) {
fun(node)
for (var i = 0, len = node.childNodes.length; i < len; i++)
traverseNode(node.childNodes[i], fun)
}
這個方法遞歸遍歷 node
的子節點,將節點交由回調函數 fun
處理。這個輔助方法在後面會用到。
核心源碼
adjacencyOperators.forEach(function(operator, operatorIndex) {
var inside = operatorIndex % 2 //=> prepend, append
$.fn[operator] = function() {
// arguments can be nodes, arrays of nodes, Zepto objects and HTML strings
var argType, nodes = $.map(arguments, function(arg) {
var arr = []
argType = type(arg)
if (argType == "array") {
arg.forEach(function(el) {
if (el.nodeType !== undefined) return arr.push(el)
else if ($.zepto.isZ(el)) return arr = arr.concat(el.get())
arr = arr.concat(zepto.fragment(el))
})
return arr
}
return argType == "object" || arg == null ?
arg : zepto.fragment(arg)
}),
parent, copyByClone = this.length > 1
if (nodes.length < 1) return this
return this.each(function(_, target) {
parent = inside ? target : target.parentNode
// convert all methods to a "before" operation
target = operatorIndex == 0 ? target.nextSibling :
operatorIndex == 1 ? target.firstChild :
operatorIndex == 2 ? target :
null
var parentInDocument = $.contains(document.documentElement, parent)
nodes.forEach(function(node) {
if (copyByClone) node = node.cloneNode(true)
else if (!parent) return $(node).remove()
parent.insertBefore(node, target)
if (parentInDocument) traverseNode(node, function(el) {
if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
(!el.type || el.type === 'text/javascript') && !el.src) {
var target = el.ownerDocument ? el.ownerDocument.defaultView : window
target['eval'].call(target, el.innerHTML)
}
})
})
})
}
調用方式
在分析之前,先看看這幾個方法的用法:
after(content)
prepend(content)
before(content)
append(content)
參數 content
可以爲 html
字符串,dom
節點,或者節點組成的數組。after
是在每個集合元素後插入 content
, before
正好相反,在每個集合元素前插入 content
,prepend
是在每個集合元素的初始位置插入 content
, append
是在每個集合元素的末尾插入 content
。before
和 after
插入的 content
在元素的外部,而 prepend
和 append
插入的 content
在元素的內部,這是需要注意的。
將參數 content
轉換成 node
節點數組
var inside = operatorIndex % 2 //=> prepend, append
遍歷 adjacencyOperators
,得到對應的方法名 operator
和方法名在數組中的索引 operatorIndex
。
定義了一個 inside
變量,當 operatorIndex
爲偶數時,inside
的值爲 true
,也就是 operator
的值爲 prepend
或 append
時,inside
的值爲 true
。這個可以用來區分 content
是插入到元素內部還是外部的方法。
$.fn[operator]
即爲 $.fn
對象設置對應的屬性值(方法名)。
var argType, nodes = $.map(arguments, function(arg) {
var arr = []
argType = type(arg)
if (argType == "array") {
arg.forEach(function(el) {
if (el.nodeType !== undefined) return arr.push(el)
else if ($.zepto.isZ(el)) return arr = arr.concat(el.get())
arr = arr.concat(zepto.fragment(el))
})
return arr
}
return argType == "object" || arg == null ?
arg : zepto.fragment(arg)
}),
變量 argType
用來保存變量變量的類型,也即 content
的類型。nodes
是根據 content
轉換後的 node
節點數組。
這裏用了 $.map
arguments
的方式來獲取參數 content
,這裏只有一個參數,這什麼不用 arguments[0]
來獲取呢?這是因爲 $.map
可以將數組進行展平,具體的實現看這裏《讀zepto源碼之工具函數》。
首先用內部函數 type
來獲取參數的類型,關於 type
的實現,在《讀Zepto源碼之內部方法》 已經作過分析。
如果參數 content
,也即 arg
的類型爲數組時,遍歷 arg
,如果數組中的元素存在 nodeType
屬性,則斷定爲 node
節點,就將其 push
進容器 arr
中;如果數組中的元素爲 zepto
對象(用 $.zepto.isZ
判斷,該方法已經在《讀Zepto源碼之神奇的
如果參數類型爲 object
(即爲 zepto
對象)或者 null
,則直接返回。
否則爲 html
字符串,調用 zepto.fragment
處理。
parent, copyByClone = this.length > 1
if (nodes.length < 1) return this
這裏還定義了 parent
變量,用來保存 content
插入的父節點;當集合中元素的數量大於 1
時,變量 copyByClone
的值爲 true
,這個變量的作用後面再說。
如果 nodes
的數量比 1
小,也即需要插入的節點爲空時,不再作後續的處理,返回 this
,以便可以進行鏈式操作。
用 insertBefore
來模擬所有操作
return this.each(function(_, target) {
parent = inside ? target : target.parentNode
// convert all methods to a "before" operation
target = operatorIndex == 0 ? target.nextSibling :
operatorIndex == 1 ? target.firstChild :
operatorIndex == 2 ? target :
null
var parentInDocument = $.contains(document.documentElement, parent)
...
})
對集合進行 each
遍歷
parent = inside ? target : target.parentNode
如果 node
節點需要插入目標元素 target
的內部,則 parent
設置爲目標元素 target
,否則設置爲當前元素的父元素。
target = operatorIndex == 0 ? target.nextSibling :
operatorIndex == 1 ? target.firstChild :
operatorIndex == 2 ? target :
null
這段是將所有的操作都用 dom
原生方法 insertBefore
來模擬。 如果 operatorIndex == 0
即爲 after
時,node
節點應該插入到目標元素 target
的後面,即 target
的下一個兄弟元素的前面;當 operatorIndex == 1
即爲 prepend
時,node
節點應該插入到目標元素的開頭,即 target
的第一個子元素的前面;當 operatorIndex == 2
即爲 before
時,insertBefore
剛好與之對應,即爲元素本身。當 insertBefore
的第二個參數爲 null
時,insertBefore
會將 node
插入到子節點的末尾,剛好與 append
對應。具體見文檔:Node.insertBefore()
var parentInDocument = $.contains(document.documentElement, parent)
調用 $.contains
方法,檢測父節點 parent
是否在 document
中。$.contains
方法在《讀zepto源碼之工具函數》中已有過分析。
將 node
節點數組插入到元素中
nodes.forEach(function(node) {
if (copyByClone) node = node.cloneNode(true)
else if (!parent) return $(node).remove()
parent.insertBefore(node, target)
...
})
如果需要複製節點時(即集合元素的數量大於 1
時),用 node
節點方法 cloneNode
來複制節點,參數 true
表示要將節點的子節點和屬性等信息也一起復制。爲什麼集合元素大於 1
時需要複製節點呢?因爲 insertBefore
插入的是節點的引用,對集合中所有元素的遍歷操作,如果不克隆節點,每個元素所插入的引用都是一樣的,最後只會將節點插入到最後一個元素中。
如果父節點不存在,則將 node
刪除,不再進行後續操作。
將節點用 insertBefore
方法插入到元素中。
處理 script
標籤內的腳本
if (parentInDocument) traverseNode(node, function(el) {
if (el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT' &&
(!el.type || el.type === 'text/javascript') && !el.src) {
var target = el.ownerDocument ? el.ownerDocument.defaultView : window
target['eval'].call(target, el.innerHTML)
}
})
如果父元素在 document
內,則調用 traverseNode
來處理 node
節點及 node
節點的所有子節點。主要是檢測 node
節點或其子節點是否爲不指向外部腳本的 script
標籤。
el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT'
這段用來判斷是否爲 script
標籤,通過 node
的 nodeName
屬性是否爲 script
來判斷。
!el.type || el.type === 'text/javascript'
不存在 type
屬性,或者 type
屬性爲 'text/javascript'
。這裏表示只處理 javascript
,因爲 type
屬性不一定指定爲 text/javascript
,只有指定爲 test/javascript
或者爲空時,纔會按照 javascript
來處理。見MDN文檔script
!el.src
並且不存在外部腳本。
var target = el.ownerDocument ? el.ownerDocument.defaultView : window
是否存在 ownerDocument
屬性,ownerDocument
返回的是元素的根節點,也即 document
對象,document
對象的 defaultView
屬性返回的是 document
對象所關聯的 window
對象,這裏主要是處理 iframe
裏的 script
,因爲在 iframe
中有獨立的 window
對象。如果不存在該屬性,則默認使用當前的 window
對象。
target['eval'].call(target, el.innerHTML)
最後調用 window
的 eval
方法,執行 script
中的腳本,腳本用 el.innerHTML
取得。
爲什麼要對 script
元素單獨進行這樣的處理呢?因爲出於安全的考慮,腳本通過 insertBefore
的方法插入到 dom
中時,是不會執行腳本的,所以需要使用 eval
來進行處理。
生成 insertAfter
、prependTo
、insertBefore
和 appendTo
方法
先來看看這幾個方法的調用方式
insertAfter(target)
insertBefore(target)
appendTo(target)
prependTo(target)
這幾個方法都是將集合中的元素插入到目標元素 target
中,跟 after
、before
、append
和 prepend
剛好是相反的操作。
他們的對應關係如下:
after => insertAfter
prepend => prependTo
before => insertBefore
append => appendTo
因此可以調用相應的方法來生成這些方法。
$.fn[inside ? operator + 'To' : 'insert' + (operatorIndex ? 'Before' : 'After')] = function(html) {
$(html)[operator](this)
return this
}
inside ? operator + 'To' : 'insert' + (operatorIndex ? 'Before' : 'After')
這段其實是生成方法名,如果是 prepend
或 append
,則在後面拼接 To
,如果是 Before
或 After
,則在前面拼接 insert
。
$(html)[operator](this)
簡單地反向調用對應的方法,就可以了。
到此,這個相似方法生成器生成了after
、 prepend
、 before
、 append
、insertAfter
、 insertBefore
、 appendTo
和 prependTo
等八個方法,相當高效。
.empty()
empty: function() {
return this.each(function() { this.innerHTML = '' })
},
empty
的作用是將所有集合元素的內容清空,調用的是 node
的 innerHTML
屬性設置爲空。
.replaceWith()
replaceWith: function(newContent) {
return this.before(newContent).remove()
},
將所有集合元素替換爲指定的內容 newContent
, newContent
的類型跟 before
的參數類型一樣。
replaceWidth
首先調用 before
將 newContent
插入到對應元素的前面,再將元素刪除,這樣就達到了替換的上的。
.wrapAll()
wrapAll: function(structure) {
if (this[0]) {
$(this[0]).before(structure = $(structure))
var children
// drill down to the inmost element
while ((children = structure.children()).length) structure = children.first()
$(structure).append(this)
}
return this
},
將集合中所有的元素都包裹進指定的結構 structure
中。
如果集合元素存在,即 this[0]
存在,則進行後續操作,否則返回 this
,以進行鏈式操作。
調用 before
方法,將指定結構插入到第一個集合元素的前面,也即所有集合元素的前面
while ((children = structure.children()).length) structure = children.first()
查找 structure
的子元素,如果子元素存在,則將 structure
賦值爲 structure
的第一個子元素,直找到 structrue
最深層的第一個子元素爲止。
將集合中所有的元素都插入到 structure
的末尾,如果 structure
存在子元素,則插入到最深層的第一個子元素的末尾。這樣就將集合中的所有元素都包裹到 structure
內了。
.wrap()
wrap: function(structure) {
var func = isFunction(structure)
if (this[0] && !func)
var dom = $(structure).get(0),
clone = dom.parentNode || this.length > 1
return this.each(function(index) {
$(this).wrapAll(
func ? structure.call(this, index) :
clone ? dom.cloneNode(true) : dom
)
})
},
爲集合中每個元素都包裹上指定的結構 structure
,structure
可以爲單獨元素或者嵌套元素,也可以爲 html
元素或者 dom
節點,還可以爲回調函數,回調函數接收當前元素和當前元素在集合中的索引兩個參數,返回符合條件的包裹結構。
var func = isFunction(structure)
判斷 structure
是否爲函數
if (this[0] && !func)
var dom = $(structure).get(0),
clone = dom.parentNode || this.length > 1
如果集合不爲空,並且 structure
不爲函數,則將 structure
轉換爲 node
節點,通過 $(structure).get(0)
來轉換,並賦給變量 dom
。如果 dom
的 parentNode
存在或者集合的數量大於 1
,則 clone
的值爲 true
。
return this.each(function(index) {
$(this).wrapAll(
func ? structure.call(this, index) :
clone ? dom.cloneNode(true) : dom
)
})
對集合進行遍歷,調用 wrapAll
方法,如果 structure
爲函數,則將回調函數返回的結果作爲參數傳給 wrapAll
;
否則,如果 clone
爲 true
,則將 dom
也即包裹元素的副本傳給 wrapAll
,否則直接將 dom
傳給 wrapAll
。這裏傳遞副本的的原因跟生成器中的一樣,也是避免對 dom
節點的引用。如果 dom
的 parentNode
存在時,表明 dom
本來就從屬於某個節點,如果直接使用 dom
,會破壞原來的結構。
.wrapInner()
wrapInner: function(structure) {
var func = isFunction(structure)
return this.each(function(index) {
var self = $(this),
contents = self.contents(),
dom = func ? structure.call(this, index) : structure
contents.length ? contents.wrapAll(dom) : self.append(dom)
})
},
將集合中每個元素的內容都用指定的結構 structure
包裹。 structure
的參數類型跟 wrap
一樣。
對集合進行遍歷,調用 contents
方法,獲取元素的內容,contents
方法在《讀Zepto源碼之集合元素查找》有過分析。
如果 structure
爲函數,則將函數返回的結果賦值給 dom
,否則將直接將 structure
賦值給 dom
。
如果 contents.length
存在,即元素不爲空元素,調用 wrapAll
方法,將元素的內容包裹在 dom
中;如果爲空元素,則直接將 dom
插入到元素的末尾,也實現了將 dom
包裹在元素的內部了。
.unwrap()
unwrap: function() {
this.parent().each(function() {
$(this).replaceWith($(this).children())
})
return this
},
當集合中的所有元素的包裹層去掉,也即將父元素去掉,但是保留父元素的子元素。
實現的方法也很簡單,就是遍歷當前元素的父元素,將父元素替換爲父元素的子元素。
.clone()
clone: function() {
return this.map(function() { return this.cloneNode(true) })
},
每集合中每個元素都創建一個副本,並將副本集合返回。
遍歷元素集合,調用 node
的原生方法 cloneNode
創建副本。要注意,cloneNode
不會將元素原來的數據和事件處理程序複製到副本中。
系列文章
參考
- Node.insertBefore()
- Node.cloneNode()
- Zepto源碼分析-zepto模塊
- MDN文檔script
- Node.ownerDocument
- Document.defaultView
License
最後,所有文章都會同步發送到微信公衆號上,歡迎關注,歡迎提意見:
作者:對角另一面