读 Zepto 源码之操纵 DOM

这篇依然是跟 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 要领来删掉当前的元素。

类似要领天生器

zeptoafterprependbeforeappendinsertAfterinsertBeforeappendToprependTo 都是经由过程这个类似要领天生器天生的。

定义容器

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

起首,定义了一个类似操纵的数组,注重数组内里只要 afterprependbeforeappend 这几个要领名,背面会看到,在天生这几个要领后,insertAfterinsertBeforeappendToprependTo 会离别挪用前面天生的几个要领。

辅佐要领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

《读 Zepto 源码之操纵 DOM》

末了,一切文章都邑同步发送到微信民众号上,迎接关注,迎接提意见: 《读 Zepto 源码之操纵 DOM》

作者:对角另一面

    原文作者:对角另一面
    原文地址: https://segmentfault.com/a/1190000009648799
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞