讀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 會分別調用前面生成的幾個方法。api

輔助方法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

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

做者:對角另外一面

相關文章
相關標籤/搜索