看Zepto如何实现增删改查DOM

前言

dom也就是文档对象模型,是针对HTML和XML的一个api,描绘了一个层次化的节点树。虽然浏览器原生给咱们提供了许多操做dom的方法,使咱们能够对dom进行查找,复制,替换和删除等操做。可是zepto在其基础上再次封装,给以咱们更加便捷的操做方式。先看下图,咱们以删除元素插入元素,复制元素包裹元素替换元素几个模块分别探究zepto如何一一将其实现。javascript

DOM操做
DOM操做

原文连接css

github项目地址 html

删除元素

remove

当父节点存在时,从其父节点中删除当前集合中的元素。java

remove: function () {
  return this.each(function () {
    if (this.parentNode != null)
      this.parentNode.removeChild(this)
  })
}复制代码

遍历当前集合中的元素,当该元素的父节点存在的时候,使用removeChild删除该元素。node

detach

功能和remove同样,都是删除元素。git

$.fn.detach = $.fn.remove复制代码

能够看到就是在$的原型上添加了一个指向remove函数的方法detachgithub

empty

清空对象集合中每一个元素的DOM内容ajax

empty: function () {
  return this.each(function () { this.innerHTML = '' })
},复制代码

遍历当前集合中的元素,而后将元素的innerHTML属性设置为空。也就达到了清除DOM内容的目的。json

插入元素

插入元素的相关api比较多,咱们先来重温部分api的使用用法和比较一下他们之间的区别。api

append, prepend, after, before

<ul class="box">
  <li>1</li>
</ul>复制代码
let $box = $('.box')
let insertDom = '<li>i am child</li>'

// append appendTo
// $box.append(insertDom)
// $(insertDom).appendTo($box)

/* <ul class="box"> <li>1</li> <li>i am child</li> </ul> */


// prepend prependTo
// $box.prepend(insertDom)
// $(insertDom).prependTo($box)

/* <ul class="box"> <li>i am child</li> <li>1</li> </ul> */

// before insertBefore
// $box.before(insertDom)
// $(insertDom).insertBefore($box)

/* <li>i am child</li> <ul class="box"> <li>1</li> </ul> */

 // after insertAfter
// $box.after(insertDom)
// $(insertDom).insertAfter($box)

/* <ul class="box"> <li>1</li> </ul> <li>i am child</li> */复制代码

以上是append,appendTo,prepend,prependTo,after,insertAfter,before,insertBefore八个方法的基本用法,以及用过以后的dom结构。咱们总结一下他们的区别。

首先每一个方法的入参均可觉得html字符串,dom节点,或者节点组成的数组。参考自zeptojs_api

append,appendTo,prepend,prependTo都是在元素内部插入内容,而after,insertAfter,before,insertBefore则是在元素外部插入内容。

append,appendTo是在元素的末尾插入内容,prepend,prependTo是在元素的初始位置插入,after,insertAfter是在元素的后面插入内容,before,insertBefore则是在元素的前面插入内容

接下来咱们开始学习和阅读实现这8大方法的核心源码部分

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

adjacencyOperators.forEach(function(operator, operatorIndex) {
  var inside = operatorIndex % 2

  $.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)
          }
        })
          })
    })
  }复制代码

遍历adjacencyOperators数组给$原型添加对应的方法

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

adjacencyOperators.forEach(function(operator, operatorIndex) {
  // xxx
  $.fn[operator] = function() {
    // xxx
  }
  // xxx
})复制代码

能够看到经过循环遍历adjacencyOperators从而给$的原型添加对应的方法。

转换node节点

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)
})复制代码

例子

// 1 html字符串
$box.append('<span>hello world</span>')
// 2 dom节点
$box.append(document.createElement('span'))
// 3 多个参数
$box.append('<span>1</span>', '<span>2</span>')
// 4 数组
$box.append(['<span>hello world</span>', document.createElement('span')])复制代码

由于传入的内容能够为html字符串,dom节点,或者节点组成的数组。这里对可能的状况分类型作了处理。经过内部的type函数判断每一个参数的数据类型并保存在argType中。

当参数类型为数组(相似上面例子中的4)的时候,再对该参数进行遍历,若是该参数中的元素存在nodeType属性则将该元素推动数组arr,
若是该参数中的元素是一个Zepto对象,则调用get方法,将arr与返回的原生元素数组进行合并。

当参数类型为object或者null的时候直接返回,不然就是处理字符串形式了,经过调用zepto.fragment(这个函数在后面的文章中会详细讲解,如今就其理解为将html字符串处理成dom节点数组就能够了)处理并将结果返回。

到如今为止,咱们已经明白了怎么将传入的content转化为对应的dom节点

接下来咱们来看如何将nodes中建立好的dom节点插入到目标位置。

parent, copyByClone = this.length > 1

if (nodes.length < 1) return this复制代码

先留意一下parent,以及copyByClone这两个变量,挺重要的,具体做用下面会详细说明。而且若是须要插入的元素数组的长度小于1,那么也就没有必要继续往下走了,直接return this进行链式操做。

return this.each(function(_, target) {
  // xxx
  nodes.forEach(function(node) {
    // xxx 
    // 注意这行,全部的插入操做都经过insertBefore函数完成
    parent.insertBefore(node, target)
    // xxx
  })

})复制代码

整个后续代码就是两层嵌套循环,第一层遍历当前选中的元素集合,第二层就是须要插入的nodes节点集合。经过两个循环来最终完成元素的插入操做,而且很重要的一点是,不论是append仍是after等方法都是经过insertBefore来模拟完成的。

肯定parent节点以及target目标节点

经过上面的分析咱们知道经过insertBefore(在当前节点的某个子节点以前再插入一个子节点)来完成节点的插入,很重要的几个因素就是

parentNode.insertBefore(newNode, referenceNode)

  1. 父节点(parentNode)
  2. 须要插入的新节点(newNode)
  3. 参考节点referenceNode

因此肯定以上1和3就显得极其重要了。怎么肯定呢?

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
  // xxx
})复制代码

inside是个啥啊!!!,让咱们回到顶部看这段

adjacencyOperators = ['after', 'prepend', 'before', 'append']
adjacencyOperators.forEach(function (operator, operatorIndex) {
  var inside = operatorIndex % 2
  // xxx
})复制代码

因此说当要往$原型上添加的方法是prependappend的时候inside为1也就是真,当为afterbefore的时候为0也就是假。

由于prependappend都是往当前选中的元素内部添加新节点,因此parent固然就是target自己了,可是afterbefore确是要往选中的元素外部添加新节点,天然parent就变成了当前选中元素的父节点。到这里上面的三要素1,已经明确了,还有3(target)如何肯定呢?

target = operatorIndex == 0 ? target.nextSibling :
  operatorIndex == 1 ? target.firstChild :
  operatorIndex == 2 ? target :
  null复制代码
  1. 若是operatorIndex为0,即after方法,node节点应该是插入到目标元素target的后面,也就是target的下一个兄弟节点的前面
  2. 若是operatorIndex为1,即prepend方法,node应该插入到目标元素target的第一个子元素的前面
  3. 若是operatorIndex为2,即before方法,node节点应该插入到target节点的前面
  4. 不然operatorIndex为4了,即append方法,node节点应该插入到target最后一个子节点的末尾,insertBefore传入null,正好与其功能相对应

好啦三要素3页已经明确了,接下来咱们把重要放在第二个循环。

将新节点插入到指定位置

nodes.forEach(function(node) {
  if (copyByClone) node = node.cloneNode(true)
  else if (!parent) return $(node).remove()

  parent.insertBefore(node, target)
  // 处理插入script状况
})复制代码

在将节点插入到指定位置的前有一个判断,若是copyByClone为真,就将要插入的新节点复制一份。为何要这么作呢?咱们来看个例子。

<ul class="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>复制代码
let $list = document.querySelector('.list')
  let $listLi = document.querySelectorAll('.list li')
  let createEle = (tagName, text) => {
    let ele = document.createElement(tagName)
    ele.innerHTML = text
    return ele
  }
  let $span1 = createEle('span', 'span1')
  let $span2 = createEle('span', 'span2')

  Array.from($listLi).forEach((target) => {
    [$span1, $span2].forEach((node) => {
      // node = node.cloneNode(true)
      $list.insertBefore(node, target)
    })
  })复制代码

先将cloneNode那部分给注销了,咱们指望往三个li的前面都插入两个span,可是结果会怎么样呢?只有最后一个节点前面能够成功地插入两个span节点。这样就不是咱们先要的结果了,根据insertBefore mdn解释,若是newElement已经在DOM树中,newElement首先会从DOM树中移除。,因此当咱们须要往多个li中插入一样相似的两个节点的时候,才须要将新节点克隆一份再插入。

咱们接着回到源码。

nodes.forEach(function(node) {
  if (copyByClone) node = node.cloneNode(true)
  else if (!parent) return $(node).remove()

  parent.insertBefore(node, target)
  // 处理插入script状况
})复制代码

若是须要(当前选中元素的个数大于1)克隆节点的时候,先将新节点克隆一份,若是没有找到对应的parent节点,就讲要插入的新节点删除,最后经过insertBefore方法插入新节点。

到了这里咱们彷佛已经完成了从

建立新节点 => 将新节点插入到指定位置的操做了。任务好像已经完成了,可是革命还没有成功,同志仍需努力啊。接下来看最后一点代码,主要是处理,当插入的节点是script
标签的时候,须要手动去执行其包含的js代码。

var parentInDocument = $.contains(document.documentElement, parent)

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)
  }
})复制代码

先提早看一下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函数。而且递归的将node节点的子节点,交给fun去处理。

接下来继续看。

首先经过$.contains方法判断parent是否在document文档中,接着须要知足一下几个条件才去执行后续操做。

  1. 存在nodeName属性
  2. nodeName是script标签
  3. type属性为空或者type属性为text/javascript
  4. src属性为空(即不指定外部脚本)

肯定window对象

var target = el.ownerDocument ? el.ownerDocument.defaultView : window复制代码

新节点存在ownerDocument mdn则window对象为defaultView mdn,不然使用window对象自己。

这里主要会考虑node节点是iframe种的元素状况,才须要作三目处理。

最后即是调用target['eval'].call(target, el.innerHTML)去执行script中的代码了。

到这里咱们终于知道了'after', 'prepend', 'before', 'append'实现全过程(偷乐一下😀,不容易啊)。

appendTo, prependTo, insertBefore, insertAfter

紧接着咱们继续往前走,前面说了插入操做有不少个方法,其中
insertAfter,insertBefore,prependTo,appendTo的实现基于上述几个方法。

// append => appendTo
// prepend => prependTo
// before => insertBefore
// after => insertAfter

$.fn[inside ? operator + 'To' : 'insert' + (operatorIndex ? 'Before' : 'After')] = function (html) {
  $(html)[operator](this)
  return this
}复制代码

若是是append或者prepend则往$原型上添加appendToprependTo方法,若是是before或者after的时候,便往$的原型上添加insertBeforeinsertAfter方法。由于其两两对应的方法本质上是一样的功能,只是在使用上有点相反的意思,因此简单的反向调用一下就能够了。

html

获取或设置对象集合中元素的HTML内容。当没有给定content参数时,返回对象集合中第一个元素的innerHtml。当给定content参数时,用其替换对象集合中每一个元素的内容。content能够是append中描述的全部类型 zeptojs_api

例子

1. html()   ⇒ string

2. html(content)   ⇒ self

3. html(function(index, oldHtml){ ... })   ⇒ self复制代码

源码实现

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参数的时候,先判断当前选中的元素是否存在,存在则读取第一个元素的innerHTML并返回,不然直接返回null

(0 in this ? this[0].innerHTML : null)复制代码

当传了html参数的时候。对当前选中的元素集合进行遍历设置,先保存当前元素的innerHTML到originHtml变量中,再将当前元素的innerHTML置空,并将funcArg函数执行以后返回的html插入到当前元素中。

function funcArg(context, arg, idx, payload) {
  return isFunction(arg) ? arg.call(context, idx, payload) : arg
}复制代码

能够看到funcArg会对传入arg进行类型判断,若是是函数,就把对应的参数传入函数再将函数的执行结果返回,不是函数就直接返回arg。

text

获取或者设置全部对象集合中元素的文本内容。当没有给定content参数时,返回当前对象集合中第一个元素的文本内容(包含子节点中的文本内容)。当给定content参数时,使用它替换对象集合中全部元素的文本内容。它有待点似 html,与它不一样的是它不能用来获取或设置 HTML。zeptojs_api

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实现方法与html比较相似有些不一样的是没有传参数的时候,html是获取第一个元素的innerHTMLtext则是将当前全部元素的textContent拼接起来并返回.

复制元素

clone

经过深度克隆来复制集合中的全部元素。zeptojs_api

clone: function () {
  return this.map(function () { return this.cloneNode(true) })
}复制代码

对当前选中的元素集合进行遍历操做,底层仍是用的浏览器cloneNode,并传参为true表示须要进行深度克隆(其实感受这里是否是将true设置为可选参数比较好呢,让使用者决定是深度克隆与否不是更合理?)

须要注意的地方是cloneNode方法不会复制添加到DOM节点中的Javascript属性,例如事件处理程序等,这个方法只复制特性,子节点,其余一切都不会复制,IE在此存在一个bug,即他会赋值事件处理程序,因此咱们建议在赋值之间最好先移除事件处理程序(摘自《JavaScript高级程序设计第三版》10.1.1 Node类型小字部分)

替换元素

replaceWidth

用给定的内容替换全部匹配的元素。(包含元素自己) zeptojs_api

replaceWith: function(newContent) {
  return this.before(newContent).remove()
}复制代码

源码实现其实很简单分两步,第一步调用前面咱们讲的before方法将制定newContent插入到元素的前面,第二部步将当前选中的元素删除。天然也就达到了替换的目的。

包裹元素

wrapAll

在全部匹配元素外面包一个单独的结构。结构能够是单个元素或 几个嵌套的元素zeptojs_api/#wrapAll

wrapAll: function (structure) {
  // 若是选中的元素存在
  if (this[0]) {
    // 则将制定structure结构经过before方法,插入到选中的第一个元素的前面
    $(this[0]).before(structure = $(structure))
    var children
    // drill down to the inmost element
    // 获取structure的最深层次的第一个子元素
    while ((children = structure.children()).length) structure = children.first()
    // 将当前的元素集合经过append方法添加到structure末尾
    $(structure).append(this)
  }
  // 反则直接返回this进行后续的链式操做
  return this
}复制代码

源码实现直接看注释就能够了,这里须要注意一下children函数是获取对象集合中全部的直接子节点。而first函数则是获取当前集合的第一个元素。

另外咱们看一下下面两个例子。

<ul class="box">
  <li>1</li>
  <li>2</li>
</ul>
<div class="wrap">
</div>
<div class="wrap">
</div>复制代码
$('.box').wrapAll('.wrap')复制代码

执行上述代码以后dom结构会变成

<div class="wrap">
  <ul class="box">
    <li>1</li>
    <li>2</li>
  </ul>
</div>

<div class="wrap">
  <ul class="box">
    <li>1</li>
    <li>2</li>
  </ul>
</div>

<ul class="box">
  <li>1</li>
  <li>2</li>
</ul>复制代码

能够看到原来ul结构仍是存在,仿佛是复制了一份ul及其子节点到wrap中被包裹起来。

接下来再看一个例子,惟一的区别就在wrap结构中嵌套了基层。

<ul class="box">
    <li>1</li>
    <li>2</li>
</ul>
<div class="wrap">
  <div class="here"></div>
  <div></div>
</div>
<div class="wrap">
  <div class="here"></div>
  <div></div>
</div>复制代码

可是最后执行$('.box').wrapAll('.wrap')获得的dom结果是。

<div class="wrap">
  <div class="here">
    <ul class="box">
      <li>1</li>
      <li>2</li>
    </ul>
  </div>
  <div></div>
</div>

<div class="wrap">
  <div class="here"></div>
  <div></div>
</div>复制代码

嘿嘿能够看到,ul原来的结构不见了,被移动到了第一个wrap的第一个子节点here中。具体缘由是什么呢?你们能够从新回去看一下append的核心实现。

wrap

在每一个匹配的元素外层包上一个html元素。structure参数能够是一个单独的元素或者一些嵌套的元素。也能够是一个html字符串片断或者dom节点。还能够是一个生成用来包元素的回调函数,这个函数返回前两种类型的包裹片断。zeptojs_api/#wrapAll

wrap: function (structure) {
  var func = isFunction(structure)
  // 当前选中的元素不为空,而且structure不是一个函数
  if (this[0] && !func)
    // 就将structure转化后的第一个元素赋值给dom元素
    var dom = $(structure).get(0),
      // 若是dom元素的parentNode存在或者当前选中的元素个数大于1那么clone为true
      clone = dom.parentNode || this.length > 1
  // 对当前选中元素进行遍历而且调用wrapAll方法
  return this.each(function (index) {
    $(this).wrapAll(
      // 若是structure为函数,则将当前的元素和对应的索引传入函数
      func ? structure.call(this, index) :
        // 若是clone为true,则使用拷贝的副本
        clone ? dom.cloneNode(true) : dom
    )
  })
}复制代码

wrapInner

将每一个元素中的内容包裹在一个单独的结构中 zeptojs_api/#wrapInner

wrapInner: function (structure) {
  // 判断structure是否为函数
  var func = isFunction(structure)
  // 对当前元素集合进行遍历处理
  return this.each(function (index) {
    // contents => 获取当前元素的全部子节点(包括元素节点和文本节点)
    var self = $(this), contents = self.contents(),
      // structure为函数则将其执行结果赋值为dom,不然直接将其赋值
      dom = func ? structure.call(this, index) : structure
      // 当前元素的子节点不为空,则调用wrapAll,不然直接将dom插入self当前元素便可
    contents.length ? contents.wrapAll(dom) : self.append(dom)
  })
}复制代码

须要注意的是这个函数和前面的wrapAll和wrap有点不同,这里强调的是将当前元素中的内容(包括元素节点和文本节点)进行包裹。

unwrap

移除集合中每一个元素的直接父节点,并把他们的子元素保留在原来的位置

unwrap: function () {
  // 经过parent()获取当前元素集合的全部直接父节点
  // 将获取到的父节点集合进行遍历
  this.parent().each(function () {
    // 将该父节点替换为该父节点的全部子节点
    $(this).replaceWith($(this).children())
  })
  return this
},复制代码

结尾

呼呼呼,终于写完了,快累死了。欢迎你们指正文中的问题。

参考

读Zepto源码之操做DOM

Zepto源码分析-zepto模块

ownerDocument

insertBefore

innerHTML

《JavaScript高级程序设计第三版》

文章记录

form模块

  1. zepto源码分析之form模块(2017-10-01)

zepto模块

  1. 这些Zepto中实用的方法集(2017-08-26)
  2. Zepto核心模块之工具方法拾遗 (2017-08-30)
  3. 看zepto如何实现增删改查DOM (2017-10-2)

event模块

  1. mouseenter与mouseover为什么这般纠缠不清?(2017-06-05)
  2. 向zepto.js学习如何手动触发DOM事件(2017-06-07)
  3. 谁说你只是"会用"jQuery?(2017-06-08)

ajax模块

  1. 原来你是这样的jsonp(原理与具体实现细节)(2017-06-11)
相关文章
相关标签/搜索