博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
看Zepto如何实现增删改查DOM
阅读量:6072 次
发布时间:2019-06-20

本文共 14243 字,大约阅读时间需要 47 分钟。

前言

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

DOM操作

删除元素

remove

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

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

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

detach

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

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

可以看到就是在$的原型上添加了一个指向remove函数的方法detach

empty

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

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

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

插入元素

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

append, prepend, after, before

  • 1
复制代码
let $box = $('.box')let insertDom = '
  • i am child
  • '// append appendTo// $box.append(insertDom)// $(insertDom).appendTo($box)/*
    • 1
    • i am child
    */// prepend prependTo// $box.prepend(insertDom)// $(insertDom).prependTo($box)/*
    • i am child
    • 1
    */// before insertBefore// $box.before(insertDom)// $(insertDom).insertBefore($box)/*
  • i am child
    • 1
    */ // after insertAfter// $box.after(insertDom)// $(insertDom).insertAfter($box)/*
    • 1
  • i am child
  • */复制代码

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

    首先每个方法的入参都可以为html字符串,dom节点,或者节点组成的数组。参考自

    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('hello world')// 2 dom节点$box.append(document.createElement('span'))// 3 多个参数$box.append('1', '2')// 4 数组$box.append(['hello world', 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 > 1if (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目标节点

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

    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为真,就将要插入的新节点复制一份。为什么要这么做呢?我们来看个例子。

    • 1
    • 2
    • 3
    复制代码
    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节点。这样就不是我们先要的结果了,根据解释,如果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复制代码

    新节点存在则window对象为,否则使用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中描述的所有类型

    例子

    1. html()   ⇒ string2. html(content)   ⇒ self3. 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。

    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

    通过深度克隆来复制集合中的所有元素。

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

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

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

    替换元素

    replaceWidth

    用给定的内容替换所有匹配的元素。(包含元素本身)

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

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

    包裹元素

    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函数则是获取当前集合的第一个元素。

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

    • 1
    • 2
    复制代码
    $('.box').wrapAll('.wrap')复制代码

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

    • 1
    • 2
    • 1
    • 2
    • 1
    • 2
    复制代码

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

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

    • 1
    • 2
    复制代码

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

    • 1
    • 2
    复制代码

    嘿嘿可以看到,ul原来的结构不见了,被移动到了第一个wrap的第一个子节点here中。具体原因是什么呢?大家可以重新回去看一下append的核心实现。

    wrap

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

    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

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

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

    结尾

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

    参考

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

    文章记录

    form模块

    1. (2017-10-01)

    zepto模块

    1. (2017-08-26)
    2. (2017-08-30)
    3. (2017-10-2)

    event模块

    1. (2017-06-05)
    2. (2017-06-07)
    3. (2017-06-08)

    ajax模块

    1. (2017-06-11)

    转载地址:http://obigx.baihongyu.com/

    你可能感兴趣的文章
    spring
    查看>>
    正方教务处抓包分析
    查看>>
    第一次作业
    查看>>
    openjudge2985(数字组合)
    查看>>
    步步为营 .NET 设计模式学习笔记 二十二、Memento(备望录模式)
    查看>>
    步步为营UML建模系列四、状态图(State)
    查看>>
    (7)javascript的程序控制结构及语句------(2)循环控制语句、跳转语句、对话框...
    查看>>
    asp.net上传图片
    查看>>
    如何修改EF的代码生成策略
    查看>>
    Yii2.0实现语言包切换功能
    查看>>
    寒假的Java学习笔记总结1
    查看>>
    C#判断操作系统的位数
    查看>>
    利用a标签自动解析URL
    查看>>
    堆,栈,字符串池,以及进程,线程浅谈内存(个人理解)
    查看>>
    sql语句(Mysql数据库)
    查看>>
    面向对象小练习
    查看>>
    Javaweb学习笔记——(二)——————CSS概述,进入JavaScript
    查看>>
    关于JDBC技术中,调用MySQL中不建议在没有服务器身份验证的情况下建立SSL连接错误解决...
    查看>>
    寻仙——向中国味表白
    查看>>
    error this is not a media message!!!
    查看>>