讀 Zepto 源碼之操做 DOM

這篇依然是跟 dom 相關的方法,側重點是操做 dom 的方法。javascript

讀Zepto源碼系列文章已經放到了github上,歡迎star: reading-zeptohtml

源碼版本

本文閱讀的源碼爲 zepto1.2.0java

.remove()

remove: function() {
  return this.each(function() {
    if (this.parentNode != null)
      this.parentNode.removeChild(this)
    })
},

刪除當前集合中的元素。node

若是父節點存在時,則用父節點的 removeChild 方法來刪掉當前的元素。git

類似方法生成器

zeptoafterprependbeforeappendinsertAfterinsertBeforeappendToprependTo 都是經過這個類似方法生成器生成的。github

定義容器

adjacencyOperators = ['after', 'prepend', 'before', 'append']

首先,定義了一個類似操做的數組,注意數組裏面只有 afterprependbeforeappend 這幾個方法名,後面會看到,在生成這幾個方法後,insertAfterinsertBeforeappendToprependTo 會分別調用前面生成的幾個方法。segmentfault

輔助方法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 是在每一個集合元素後插入 contentbefore 正好相反,在每一個集合元素前插入 contentprepend 是在每一個集合元素的初始位置插入 contentappend 是在每一個集合元素的末尾插入 contentbeforeafter 插入的 content 在元素的外部,而 prependappend 插入的 content 在元素的內部,這是須要注意的。微信

將參數 content 轉換成 node 節點數組

var inside = operatorIndex % 2 //=> prepend, append

遍歷 adjacencyOperators,獲得對應的方法名 operator 和方法名在數組中的索引 operatorIndex

定義了一個 inside 變量,當 operatorIndex 爲偶數時,inside 的值爲 true,也就是 operator 的值爲 prependappend 時,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源碼之神奇的$》有過度析),不傳參調用 get 方法,返回的是一個數組,而後調用數組的 concat 方法合併數組,get 方法在《讀Zepto源碼之集合操做》有過度析;不然,爲 html 字符串,調用 zepto.fragment 處理,並將返回的數組合並,`zepto.fragment 在《讀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 標籤,經過 nodenodeName 屬性是否爲 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)

最後調用 windoweval 方法,執行 script 中的腳本,腳本用 el.innerHTML 取得。

爲何要對 script 元素單獨進行這樣的處理呢?由於出於安全的考慮,腳本經過 insertBefore 的方法插入到 dom 中時,是不會執行腳本的,因此須要使用 eval 來進行處理。

生成 insertAfterprependToinsertBeforeappendTo 方法

先來看看這幾個方法的調用方式

insertAfter(target)
insertBefore(target)
appendTo(target)
prependTo(target)

這幾個方法都是將集合中的元素插入到目標元素 target 中,跟 afterbeforeappendprepend 恰好是相反的操做。

他們的對應關係以下:

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')

這段實際上是生成方法名,若是是 prependappend ,則在後面拼接 To ,若是是 BeforeAfter,則在前面拼接 insert

$(html)[operator](this)

簡單地反向調用對應的方法,就能夠了。

到此,這個類似方法生成器生成了afterprependbeforeappendinsertAfterinsertBeforeappendToprependTo 等八個方法,至關高效。

.empty()

empty: function() {
  return this.each(function() { this.innerHTML = '' })
},

empty 的做用是將全部集合元素的內容清空,調用的是 nodeinnerHTML 屬性設置爲空。

.replaceWith()

replaceWith: function(newContent) {
  return this.before(newContent).remove()
},

將全部集合元素替換爲指定的內容 newContentnewContent 的類型跟 before 的參數類型同樣。

replaceWidth 首先調用 beforenewContent 插入到對應元素的前面,再將元素刪除,這樣就達到了替換的上的。

.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
      )
    })
},

爲集合中每一個元素都包裹上指定的結構 structurestructure 能夠爲單獨元素或者嵌套元素,也能夠爲 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。若是 domparentNode 存在或者集合的數量大於 1 ,則 clone 的值爲 true

return this.each(function(index) {
  $(this).wrapAll(
  func ? structure.call(this, index) :
  clone ? dom.cloneNode(true) : dom
  )
})

對集合進行遍歷,調用 wrapAll 方法,若是 structure 爲函數,則將回調函數返回的結果做爲參數傳給 wrapAll

不然,若是 clonetrue ,則將 dom 也即包裹元素的副本傳給 wrapAll ,不然直接將 dom 傳給 wrapAll。這裏傳遞副本的的緣由跟生成器中的同樣,也是避免對 dom 節點的引用。若是 domparentNode 存在時,代表 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 不會將元素原來的數據和事件處理程序複製到副本中。

系列文章

  1. 讀Zepto源碼之代碼結構

  2. 讀 Zepto 源碼以內部方法

  3. 讀Zepto源碼之工具函數

  4. 讀Zepto源碼之神奇的$

  5. 讀Zepto源碼之集合操做

  6. 讀Zepto源碼之集合元素查找

參考

License

License: CC BY-NC-ND 4.0

最後,全部文章都會同步發送到微信公衆號上,歡迎關注,歡迎提意見:

做者:對角另外一面

相關文章
相關標籤/搜索