资讯专栏INFORMATION COLUMN

读 Zepto 源码之操作 DOM

beita / 2624人阅读

摘要:辅助方法这个方法递归遍历的子节点,将节点交由回调函数处理。对集合进行遍历,调用方法,如果为函数,则将回调函数返回的结果作为参数传给否则,如果为,则将也即包裹元素的副本传给,否则直接将传给。

这篇依然是跟 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文档

最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

作者:对角另一面

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/83308.html

相关文章

  • Zepto源码Data模块

    摘要:的模块用来获取节点中的属性的数据,和储存跟相关的数据。获取节点指定的缓存值。如果存在,则删除指定的数据,否则将缓存的数据全部删除。为所有下级节点,如果为方法,则节点自身也是要被移除的,所以需要将自身也加入到节点中。 Zepto 的 Data 模块用来获取 DOM 节点中的 data-* 属性的数据,和储存跟 DOM 相关的数据。 读 Zepto 源码系列文章已经放到了github上,欢...

    wua_wua2012 评论0 收藏0
  • Zepto源码Form模块

    摘要:模块处理的是表单提交。表单提交包含两部分,一部分是格式化表单数据,另一部分是触发事件,提交表单。最终返回的结果是一个数组,每个数组项为包含和属性的对象。否则手动绑定事件,如果没有阻止浏览器的默认事件,则在第一个表单上触发,提交表单。 Form 模块处理的是表单提交。表单提交包含两部分,一部分是格式化表单数据,另一部分是触发 submit 事件,提交表单。 读 Zepto 源码系列文章已...

    陈江龙 评论0 收藏0
  • Zepto源码代码结构

    摘要:源码结构整体结构如果在编辑器中将的源码折叠起来,看到的就跟上面的代码一样。参考源码分析代码结构对象思想与源码分析设计和源码分析源码中关于的问题最后,所有文章都会同步发送到微信公众号上,欢迎关注欢迎提意见 虽然最近工作中没有怎么用 zepto ,但是据说 zepto 的源码比较简单,而且网上的资料也比较多,所以我就挑了 zepto 下手,希望能为以后阅读其他框架的源码打下基础吧。 源码版...

    warkiz 评论0 收藏0
  • Zepto源码Stack模块

    摘要:读源码系列文章已经放到了上,欢迎源码版本本文阅读的源码为改写原有的方法模块改写了以上这些方法,这些方法在调用的时候,会为返回的结果添加的属性,用来保存原来的集合。方法的分析可以看读源码之模块。 Stack 模块为 Zepto 添加了 addSelf 和 end 方法。 读 Zepto 源码系列文章已经放到了github上,欢迎star: reading-zepto 源码版本 本文阅读的...

    crossea 评论0 收藏0
  • Zepto源码assets模块

    摘要:模块是为解决移动版加载图片过大过多时崩溃的问题。因为没有处理过这样的场景,所以这部分的代码解释不会太多,为了说明这个问题,我翻译了这篇文章作为附文怎样处理移动端对图片资源的限制,更详细地解释了这个模块的应用场景。 assets 模块是为解决 Safari 移动版加载图片过大过多时崩溃的问题。因为没有处理过这样的场景,所以这部分的代码解释不会太多,为了说明这个问题,我翻译了《How to...

    thursday 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<