前言
dom也就是文档对象模型,是针对HTML和XML的一个api,描绘了一个层次化的节点树。虽然浏览器原生给我们提供了许多操作dom的方法,使我们可以对dom进行查找,复制,替换和删除等操作。但是zepto在其基础上再次封装,给以我们更加便捷的操作方式。先看下图,我们以
删除元素
,插入元素
,复制元素
,包裹元素
和替换元素
几个模块分别探究zepto如何一一将其实现。
删除元素
remove
当父节点存在时,从其父节点中删除当前集合中的元素。
|
|
遍历当前集合中的元素,当该元素的父节点存在的时候,使用removeChild
删除该元素。
detach
功能和remove一样,都是删除元素。
|
|
可以看到就是在$的原型上添加了一个指向remove
函数的方法detach
。
empty
清空对象集合中每个元素的DOM内容
|
|
遍历当前集合中的元素,然后将元素的innerHTML属性设置为空。也就达到了清除DOM内容的目的。
插入元素
插入元素的相关api比较多,我们先来重温部分api的使用用法和比较一下他们之间的区别。
append, prepend, after, before
|
|
|
|
以上是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数组给$原型添加对应的方法
|
|
可以看到通过循环遍历adjacencyOperators
从而给$的原型添加对应的方法。
转换node节点
|
|
例子
|
|
因为传入的内容可以为html字符串,dom节点,或者节点组成的数组。这里对可能的情况分类型做了处理。通过内部的type
函数判断每个参数的数据类型并保存在argType
中。
当参数类型为数组(类似上面例子中的4)的时候,再对该参数进行遍历,如果该参数中的元素存在nodeType
属性则将该元素推进数组arr,
如果该参数中的元素是一个Zepto对象
,则调用get方法,将arr与返回的原生元素数组进行合并。
当参数类型为object
或者null
的时候直接返回,否则就是处理字符串形式了,通过调用zepto.fragment(这个函数在后面的文章中会详细讲解,现在就其理解为将html字符串处理成dom节点数组就可以了)处理并将结果返回。
到现在为止,我们已经明白了怎么将传入的content
转化为对应的dom节点
。
接下来我们来看如何将nodes
中创建好的dom节点插入到目标位置。
|
|
先留意一下parent
,以及copyByClone
这两个变量,挺重要的,具体作用下面会详细说明。并且如果需要插入的元素数组的长度小于1,那么也就没有必要继续往下走了,直接return this
进行链式操作。
|
|
整个后续代码就是两层嵌套循环,第一层遍历当前选中的元素集合,第二层就是需要插入的nodes节点集合。通过两个循环来最终完成元素的插入操作,并且很重要的一点是,不管是append
还是after
等方法都是通过insertBefore
来模拟完成的。
确定parent节点以及target目标节点
通过上面的分析我们知道通过insertBefore(在当前节点的某个子节点之前再插入一个子节点)来完成节点的插入,很重要的几个因素就是
parentNode.insertBefore(newNode, referenceNode)
- 父节点(parentNode)
- 需要插入的新节点(newNode)
- 参考节点referenceNode
所以确定以上1和3就显得极其重要了。怎么确定呢?
|
|
inside是个啥啊!!!,让我们回到顶部看这段
|
|
所以说当要往$原型上添加的方法是prepend
和append
的时候inside
为1也就是真,当为after
和before
的时候为0也就是假。
因为prepend
和append
都是往当前选中的元素内部添加新节点,所以parent
当然就是target
本身了,但是after
和before
确是要往选中的元素外部添加新节点,自然parent
就变成了当前选中元素的父节点。到这里上面的三要素1,已经明确了,还有3(target)如何确定呢?
|
|
- 如果operatorIndex为0,即after方法,node节点应该是插入到目标元素target的后面,也就是target的下一个兄弟节点的前面
- 如果operatorIndex为1,即prepend方法,node应该插入到目标元素target的第一个子元素的前面
- 如果operatorIndex为2,即before方法,node节点应该插入到target节点的前面
- 否则operatorIndex为4了,即append方法,node节点应该插入到target最后一个子节点的末尾,insertBefore传入null,正好与其功能相对应
好啦三要素3页已经明确了,接下来我们把重要放在第二个循环。
将新节点插入到指定位置
|
|
在将节点插入到指定位置的前有一个判断,如果copyByClone
为真,就将要插入的新节点复制一份。为什么要这么做呢?我们来看个例子。
|
|
|
|
先将cloneNode那部分给注销了,我们期望往三个li的前面都插入两个span,但是结果会怎么样呢?只有最后一个节点前面可以成功地插入两个span节点。这样就不是我们先要的结果了,根据insertBefore mdn解释,如果newElement已经在DOM树中,newElement首先会从DOM树中移除。,所以当我们需要往多个li中插入同样类似的两个节点的时候,才需要将新节点克隆一份再插入。
我们接着回到源码。
|
|
如果需要(当前选中元素的个数大于1)克隆节点的时候,先将新节点克隆一份,如果没有找到对应的parent节点,就讲要插入的新节点删除,最后通过insertBefore
方法插入新节点。
到了这里我们似乎已经完成了从
创建新节点
=> 将新节点插入到指定位置
的操作了。任务好像已经完成了,但是革命尚未成功,同志仍需努力啊。接下来看最后一点代码,主要是处理,当插入的节点是script
标签的时候,需要手动去执行其包含的js代码。
|
|
先提前看一下traverseNode这个函数的代码
|
|
这个函数的主要作用就是将传入的node节点作为参数去调用传入的fun函数。并且递归的将node节点的子节点,交给fun去处理。
接下来继续看。
首先通过$.contains
方法判断parent
是否在document
文档中,接着需要满足一下几个条件才去执行后续操作。
- 存在nodeName属性
- nodeName是script标签
- type属性为空或者type属性为text/javascript
- src属性为空(即不指定外部脚本)
确定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
或者prepend
则往$原型上添加appendTo
和prependTo
方法,如果是before
或者after
的时候,便往$的原型上添加insertBefore
和insertAfter
方法。因为其两两对应的方法本质上是同样的功能,只是在使用上有点相反的意思,所以简单的反向调用一下就可以了。
html
获取或设置对象集合中元素的HTML内容。当没有给定content参数时,返回对象集合中第一个元素的innerHtml。当给定content参数时,用其替换对象集合中每个元素的内容。content可以是append中描述的所有类型 zeptojs_api
例子
|
|
源码实现
|
|
当没有传html参数的时候,先判断当前选中的元素是否存在,存在则读取第一个元素的innerHTML并返回,否则直接返回null
|
|
当传了html参数的时候。对当前选中的元素集合进行遍历设置,先保存当前元素的innerHTML到originHtml变量中,再将当前元素的innerHTML置空,并将funcArg函数执行之后返回的html插入到当前元素中。
|
|
可以看到funcArg会对传入arg进行类型判断,如果是函数,就把对应的参数传入函数再将函数的执行结果返回,不是函数就直接返回arg。
text
获取或者设置所有对象集合中元素的文本内容。当没有给定content参数时,返回当前对象集合中第一个元素的文本内容(包含子节点中的文本内容)。当给定content参数时,使用它替换对象集合中所有元素的文本内容。它有待点似 html,与它不同的是它不能用来获取或设置 HTML。zeptojs_api
|
|
text实现方法与html比较类似有些不同的是没有传参数的时候,html是获取第一个元素的innerHTMLtext则是将当前所有元素的textContent拼接起来并返回.
复制元素
clone
通过深度克隆来复制集合中的所有元素。zeptojs_api
|
|
对当前选中的元素集合进行遍历操作,底层还是用的浏览器cloneNode,并传参为true表示需要进行深度克隆(其实感觉这里是不是将true设置为可选参数比较好呢,让使用者决定是深度克隆与否不是更合理?)
需要注意的地方是cloneNode方法不会复制添加到DOM节点中的Javascript属性,例如事件处理程序等,这个方法只复制特性,子节点,其他一切都不会复制,IE在此存在一个bug,即他会赋值事件处理程序,所以我们建议在赋值之间最好先移除事件处理程序(摘自《JavaScript高级程序设计第三版》10.1.1 Node类型小字部分)
替换元素
replaceWidth
用给定的内容替换所有匹配的元素。(包含元素本身) zeptojs_api
|
|
源码实现其实很简单分两步,第一步调用前面我们讲的before方法将制定newContent插入到元素的前面,第二部步将当前选中的元素删除。自然也就达到了替换的目的。
包裹元素
wrapAll
在所有匹配元素外面包一个单独的结构。结构可以是单个元素或 几个嵌套的元素zeptojs_api/#wrapAll
|
|
源码实现直接看注释就可以了,这里需要注意一下children
函数是获取对象集合中所有的直接子节点。而first
函数则是获取当前集合的第一个元素。
另外我们看一下下面两个例子。
|
|
|
|
执行上述代码之后dom结构会变成
|
|
可以看到原来ul结构还是存在,仿佛是复制了一份ul及其子节点到wrap中被包裹起来。
接下来再看一个例子,唯一的区别就在wrap结构中嵌套了基层。
|
|
但是最后执行$('.box').wrapAll('.wrap')
得到的dom结果是。
|
|
嘿嘿可以看到,ul原来的结构不见了,被移动到了第一个wrap的第一个子节点here中。具体原因是什么呢?大家可以重新回去看一下append的核心实现。
wrap
在每个匹配的元素外层包上一个html元素。structure参数可以是一个单独的元素或者一些嵌套的元素。也可以是一个html字符串片段或者dom节点。还可以是一个生成用来包元素的回调函数,这个函数返回前两种类型的包裹片段。zeptojs_api/#wrapAll
|
|
wrapInner
将每个元素中的内容包裹在一个单独的结构中 zeptojs_api/#wrapInner
|
|
需要注意的是这个函数和前面的wrapAll和wrap有点不一样,这里强调的是将当前元素中的内容(包括元素节点和文本节点)进行包裹。
unwrap
移除集合中每个元素的直接父节点,并把他们的子元素保留在原来的位置
|
|
结尾
呼呼呼,终于写完了,快累死了。欢迎大家指正文中的问题。
参考
《JavaScript高级程序设计第三版》
文章记录
form模块
- zepto源码分析之form模块(2017-10-01)
zepto模块
- 这些Zepto中实用的方法集(2017-08-26)
- Zepto核心模块之工具方法拾遗 (2017-08-30)
- 看zepto如何实现增删改查DOM (2017-10-2)
event模块
- mouseenter与mouseover为何这般纠缠不清?(2017-06-05)
- 向zepto.js学习如何手动触发DOM事件(2017-06-07)
- 谁说你只是”会用”jQuery?(2017-06-08)
ajax模块
- 原来你是这样的jsonp(原理与具体实现细节)(2017-06-11)