jquery 源码解析 节点遍历

时间:2023-03-09 18:43:13
jquery 源码解析  节点遍历

jquery遍历,用于根据其相对于其他元素的关系来查找或选取html元素,以某项选择开始,并沿着这个选择移动,知道移动被称为对dom进行遍历

jquery 源码解析  节点遍历

☑ <div> 元素是 <ul> 的父元素,同时是其中所有内容的祖先。

☑ <ul> 元素是 <li> 元素的父元素,同时是 <div> 的子元素

☑ 左边的 <li> 元素是 <span> 的父元素,<ul> 的子元素,同时是 <div> 的后代。

☑ <span> 元素是 <li> 的子元素,同时是 <ul> 和 <div> 的后代。

☑ 两个 <li> 元素是同胞(拥有相同的父元素)。

☑ 右边的 <li> 元素是 <b> 的父元素,<ul> 的子元素,同时是 <div> 的后代。

☑ <b> 元素是右边的 <li> 的子元素,同时是 <ul> 和 <div> 的后代。

提示:祖先是父、祖父、曾祖父等等。后代是子、孙、曾孙等等。同胞拥有相同的父。

jQuery的遍历处理不仅只是针对基本的层级关系,还扩展了大量的筛选接口,包括了用于筛选、查找和串联元素的方法。之后我们会介绍到各自的实现。

jquery的遍历节后设计

遍历的接口分为:祖先,同胞兄弟,后代,过滤。

1,针对层级关系的处理,jquery就抽出了一个dir的方法,用于根据传递的元素与词素的位置关系,查找指定的元素

function dir(elem.dir,until){

  return matched;

}

迭代器除了单纯的遍历,在jquery内部的运用最多的就是借口的抽象合并,相同功能的代码功能合并处理:

jquery.each({

  parent:function(elem){

    var parent = elem.parentNode;

    return parent && parent.nodeType !==11 ? parent :null;

  },

  parents:function(elem){

    return dir(elem,"parentNode");

  },

  nextAll:function(elem){},

  prevAll:function(elem){},

  .......

},function(name,fn){

  ajQuery[name] = function(until,selector){

    return fn(until.selector);

  }

})

节点操作

jquery对节点的操作分装除了一系列的接口,可以接受html字符,dom元素,元素数组,或者jquery对象,用来插在集合中每个匹配元素的不同位置

html结构:$(".inner").after("<p>Test</p>");

$对象:$(".container").after($("h2"));

回调函数

一个返回html字符串,dom元素,或者jquery对象的函数,插在每个匹配元素的后面,接收元素在集合中的索引位置作为参数,在函数中this指向员集合中的当前元素。

$("p").after(function(){

  return '<div>+this.className+</div>';

});

源码:

append:function(){

  return this.domManip(arguments,function(elem){

    if(this.nodeType ===1 || this.nodeType === 11 this.nodeType ===9){

      var target = mainpulationTarget(this,elem);

      target.appendChild(elem);

    }

  });

},

prepend:function(){

  return this.domManip(arguments,function(elem){

    if(this.nodeType ===1 || this.nodeType ===11 || this.nodeType ===9){

      var target = manipulationTarget(this,elem);

      target.insertBefore(elem,target.fristChild);

    }

  })

},

.domManip()是jquery DOM操作的核心函数,dom是DOM元素,Manip是Manipulate的缩写,连在一起就是DOM操作的意思。

对封装的节点操作做了参数上的校正支持,与对应处理的调用,append,prepend,before,after,replaceWith,appendTo,prependTo,insertBefore,insertAfter,replaceAll.

通过把一个节点增加到当前节点的childNodes[]组,给文档数增加节点:appendChild()

复制当前节点,或者复制当前节点以及他的所有子孙节点:cloneNode()如果当前节点拥有子节点,则返回true:hasChildNodes()

给文档数插入一个节点,位置在当前节点的指定子节点之前,如果该节点已经存在,则删除再插入到他的位置:removeChild()

从文档树中删除并返回指定的子节点:replaceChild()

针对节点操作的几个重要的细节

1,保证最终操作的永远是dom元素,浏览器的最终api只认识那么几个接口,所以如果传递是字符串或者其他的,当然需要转换。

2,针对节点的大量操作, 我们肯定是需要引入文档碎片做优化的,这个必不可少。

我们 知道jquery的借口是支持多种参数传递的,那么就需要有一个过滤器的中介来处理各种参数类型

domManip在设计上需要做的处理:

1,解析参数,字符串,函数,对象。

2,针对大数据引入文档碎片处理

3,如果参数中包含script的处理

其中还有很多细节的问题:

1,IE下面innerHTML会忽略没作用域元素, no-scope element(script,link,style,meta,noscript)等,所以这类通过innerHTML穿件需要加前缀

2,innerHTML在IE下面会对字符串进行trimLeft操作,去掉空白

3,innerHTML不会执行脚本的代码,如果支持defer除外

4,很多标签不能作为div的子元素,td,tr,tbody等等

5.jquery是合集对象,文档碎片的与事件的复制问题

div.innerHTML = "<script>alert(1)</script>";//这个代码不会执行

例外的情况,ie9之前满足几个条件:

1,script设定defer属性

2,script元素必须位于有作用域元素之后,因为script被认为是无作用域的(页面不可见)

div.innerHTML = "<div><script defer>alter(1)</script></div>";//可以执行

jquery在内部通过会有一个domManip方法,把这些问题都方方面面处理掉了

jquery是兼容ie6浏览器的,append,prepend,before,after都是操作Dom元素的函数,如果被插入的对象是table,那就有要在table中加入tbody标签

jquery 源码解析  节点遍历

1,函数调用了domManip函数,传进去的参数第一个是arguments,arguments是函数参数对象,是一个类数组对象,这里的arguments可能是包含dom元素的数组,或者html字符串

2,第二参数是一个回调函数,target.appendChild(elem);在回调函数中分离的处理方法, 通过domManip抽象出公共的处理,其余的prepend,before,after等接口也是类似处理。

例子:

var div = document.querySelectorAll('div')[0];

div.innnerHTML="<script>alert(1)</script>";这样的JavaScript不会执行,能插入script标签,如果换成jquery的append方法,JavaScript就执行了。

其中一个.html()方法就不会用innerHTML操作,而是用jQuery.append()处理字符串塞入

append domManip buildFragment clean 这样的处理流程

clean中会动态产生一个div,将div的innerHTML设为传入的字符串,再用getElementsTagName("script")的方式把所有的script抓出来执行

如果是外部js就动态载入,如果是内联js就用eval()

总结下来,domManip主要就做了两件事

1,根据用户传入的参数,穿件了多个fragment,然后通过回调函数参数传入

2,控制script的执行过程, 在创建fragment的时候不执行,最后操作结束后会统一执行

innerHTML的缺陷

对于节点的创建innerHTML是一个很搞笑的接口,jquery在节点操作上使用了innerHTML,创建了效率上来说至少比createElement快乐2-10倍不等,而且还能一次性生成一堆的节点,但是随之而来就有一些兼容性问题。

兼容上也结合了jquery的解决方案

1.ie会对用于字符串进行trimLeft操作,用户可能的本意就是空白。

2.ie8有些元素innerHTML是只读。

3,ie会忽略开头的无作用域元素

4.大多情况下不执行scrupt脚本,当然如果只选defer的ie9之前的浏览器除外

5.一些标签不能作为div的子元素,如tr,td,col等

jquery的节点操作最终是需要转化成文档碎片也就是要通过bulidFragment方法处理的,所以innerHTML兼容的修复在bulidFragment方法中。

1,首先无作用域的问题,通过文档碎片穿件一个div的包含容器,让所有的元素被div元素包含起来,包括script,style等作用域的元素,很好的解决了

tmp = tmp || framgment.appendChild(context.createElement('div'));

tmp.innerHTML = elem;

2,针对不支持innerHTML属性的元素,给单独提出来,通过正则出来这个节点名字去处理。

wrapMap = {
tr: [2, "<table><tbody>", "</tbody></table>"],

tag = /<([\w:]+)/.exec(‘<tr>慕课网</tr>’)
wrap = wrapMap[tag] || wrapMap._default;
tmp.innerHTML = wrap[1] + elem.replace(rxhtmlTag, "<$1></$2>") + wrap[2];

文档碎片DomcumentFragment

什么是文档碎片

DocumentFragment是一个轻量级的文档对象,能够提取部分文档的树或创建一个新的文档片段,换句话说有文档缓存的作用

createDocumentFragment是什么作用

多次使用节点方法(如:appendChild)绘制页面,每次都要刷新页面一次,效率也就大打折扣了,而使用document_createDocumentFragment创建一个文档碎片,把所有的新节点附加在上面,然后把文档碎片的内容一次性添加到document中,这就只需要一次页面刷新就可以了。

DoucumentFragment类型

在所有节点类型中,只有DocumentFragment在文档中没有对应的标记,DOM规定文档片段是一个轻量级的文档,可以包含和控制节点,但不会像完整的文档那样占用额外资源,DocumentFragment节点具有下列特性

1,nodeType的值为11

2,nodeName的值有#doment-fragment

3,nodeValue为null

4,parentNode的值为null

5,子节点可以是Element,processingInstruction,comment,text,CDATASection或EntityReference

虽然不能把文档片段直接添加到文档中,但可以将他作为一个仓库来使用,即可以在里面保存将来可能会添加到文档中的节点,要创建文档片段,可以使用document.createDocumentFragment方法

var fragment = documeny.crateDocumentFfragment()

文档片段继承了node的所有方法,通过用于执行那些针对文档的DOM操作, 如果将文档中的节点添加到文档片段中,就会从文档树中在看到该节点,添加到文档片段中的新节点同样也不属于文档树,可以通过appendChild或insertBefore将文档片段中内容添加到文档中,再将文档片段作为参数传递给这两个方法时, 实际上只会将文档片段所有子节点添加到相应的位置上,文档片段本身永远不会称为文档树的一部分

http://www.w3cmm.com/dom/documentfragment.html

createElement是创建一个新的节点,createDocumentFragment是创建一个文档片段

DocumentFragment接口表示文档的一部分,更确切的说,他表示一个或多个邻接的Document节点和他们的所有子孙节点

DocumentFragment节点不属于文档树,继承的是parentNode属性总是null

不过他有一种特殊的行为,该行为使得他非常有用

即当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment自身,而是他的所有子孙节点,这使得

这使得 DocumentFragment 成了有用的占位符,暂时存放那些一次插入文档的节点。它还有利于实现文档的剪切、复制和粘贴操作,尤其是与 Range 接口一起使用时更是如此。

可以用 Document.createDocumentFragment() 方法创建新的空 DocumentFragment 节点。

也可以用 Range.extractContents() 方法 或 Range.cloneContents() 方法 获取包含现有文档的片段的 DocumentFragment 节点。

除此之外,createElement创建的元素可以使用innerHTML,createDocumentFragment创建的元素使用innerHTML并不能达到预期修改文档内容的效果,只是作为一个属性而已。两者的节点类型完全不同,并且createDocumentFragment创建的元素在文档中没有对应的标记,因此在页面上只能用js中访问到。

createElement创建的元素可以重复操作,添加之后就算从文档里面移除依旧归文档所有,可以继续操作,但是createDocumentFragment创建的元素是一次性的,添加之后再就不能操作了,在之前domManip方法中提到的iNoClone多个节点操作需要克隆,就是因为文档碎片的特性引起的,大体了解了,我们看看jQuery对于节点操作的时候,加强版的文档碎片buildFragment。

核心bulidFragment

DOM原生的接口是简单又单一,参数类型确定,也不会重载,每次只会处理一个元素,如果一次传递N个节点元素那么在处理上要优化就必须引入文档碎片了。

创建文档碎片

fragment = context.createDocumentFragment()

然后把所有需要处理的dom节点的appendChild进去

bulidFragment对于文档碎片的创建,可以看到被切分了2个部分

先看第一部分代码,收集节点元素:

var $newdiv1 = $('<div id="object1"/>'),
newdiv2 = document.createElement('div'),
existingdiv1 = document.getElementById('foo');
$('body').append($newdiv1, [newdiv2, existingdiv1,'<td>慕课网</td>','文本','<script>alert(1)']) 这段代码包含了6中不同类型的参数,基本覆盖了所有的bulidFragment的处理其实很简单,我们只需要把不同类型的参数分解后,压入到文档碎片就可以了,当然因为类型的不同处理的方式也有不同。
eg
ie对字符串进行trimLeft操作,其余是用户输入处理,很多标签不能单独作为div的子元素,td,th,tr,tfoot,tbody等等,需要加头尾,
<td>慕课网</td>

jQuery通过wrapMap转化成,否则有些会当成普通文本来解释:

"<table><tbody><tr><td>慕课网</td></tr></tbody></table>"

我们参考右边的代码,整个流程如下:

  1. 分解类型,jQuery对象,节点对象,文本,字符串,脚本
  2. 引入nodes收集各种分解的类型数据
  3. 针对html节点,兼容IE的处理,先过滤空白,然后补全tr,td等
  4. 创建文档碎片的div包含节点,把html结构给innerHTML进去
  5. 取出创建的节点,jQuery.merge(nodes, tmp.childNodes),因为靠div包装过

 

插入

DOM插入有关的方法

1,innerHTML设置或获取位于对象起始和结束标签内的html

2,outerHTML设置或获取对象及其内容的html形式

3,innerText和outerText在读取的时候是一样的,只是在设置的时候outer Text

fixfox不支持innerText,但是可以用textContent代替

jquery封装的方法html,text,val

.html()用为读取和修改元素的html元素

.text()用来读取或修改元素的纯文本内容

.val()用来读取或修改表单元素的value

html

获取集合中第一个匹配元素的html内容

取值

在一盒html文档中,我们可以使用.html方法来获取任何一个元素的内容,如果选择匹配多个元素,那么只有第一个元素的html内容会被获取

jquery针对DOM操作的插入的方法大概有10种

append,prepend,before,after,replaceWith,appendTo,prependTo,insertBefore,insertAfter,replaceAll

after:选择表达式在函数的前面,参数是将要插入的内容

insertAfter:刚好相反,内容在方法前面,他将被放在参数里元素的后面

After

after: function() {
    return this.domManip( arguments, function( elem ) {
        if ( this.parentNode ) {
            this.parentNode.insertBefore( elem, this.nextSibling );
        }
    });
},

之前提过了所有的方法靠this.domManip合并参数处理,内部通过buildFragment模块构建文档碎片,然后把每一个方法的具体执行通过回调的方式提供出来处理。

DOM操作并未提供一个直接可以在当前节点后插入一个兄弟节点的方法,但是提供了一个类似的方法。

insertBefore() 方法:可在已有的子节点前插入一个新的子节点。

语法 :insertBefore(newchild,refchild)

看看jQuery如何处理的,例如:

inner.after('<p>Test</p>');

内部就会把  '<p>Test</p>' 通过buildFragment构建出文档elem,然后通过 this.parentNode.insertBefore( elem, this.nextSibling );

这里的this 就是对应着inner ,elem就是‘<p>Test</p>’,看到这里就很好理解after的实现了。

用原生方法简单模拟:右侧代码编辑器所示。

insertAfter

$('<p>Test</p>').insertAfter('.inner');

通过$('<p>Test</p>')构建一个文档,对象通过insertAfter方法插入到所有class等于inner的节点后。表达的意思与after是一样的,主要的不同是语法——特别是内容和目标的位置。

看具体的实现方法中insertAfter('.inner');inner其实就被当作selector传入进来了,selector可能只是字符串选择器内部就需要转化,insert = jQuery( selector ),

$('<p>Test</p>')就是构建出来的文档碎片节点,那么如果赋给insert有多个的时候就需要完全克隆一份副本了,所以就直接赋给:

elems = i === last ? this : this.clone( true );
jQuery( insert[ i ] )[ original ]( elems );

依旧是执行after:

jQuery( insert[ i ] )[ original ]( elems );

最终还需要返回这个构建的新节点。

收集构建的节点:

core_push.apply( ret, elems.get() );

构建一个新jQuery对象,以便实现链式:

this.pushStack( ret );

可见 after 与 insertAfter 本质其实都是一样的,只是通过不同的方式调用。

before()

根据参数设定,在匹配元素的前面插入内容:

before: function() {
return this.domManip( arguments, function( elem ) {
if ( this.parentNode ) {
this.parentNode.insertBefore( elem, this );
}
});
}

类似after只是替换了第二个参数,改变插入的位置。

append()

在每个匹配元素里面的末尾处插入参数内容:

append: function() {
return this.domManip( arguments, function( elem ) {
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
var target = manipulationTarget( this, elem );
target.appendChild( elem );
}
});
}

内部增加节点,直接可以调用appendChild方法。

prepend()

prepend: function() {
return this.domManip( arguments, function( elem ) {
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
var target = manipulationTarget( this, elem );
target.insertBefore( elem, target.firstChild );
}
});
},

类似after只是替换了第二个参数,改变插入的位置。

替换

replaceWith()

用提供的内容替换集合中所有匹配的元素并且返回被删除元素的集合。

.replaceWith()可以从DOM中移除内容,然后在这个地方插入新的内容。

通过调用replaceChild处理即可,但是这里需要注意的问题,就是事件的处理,因为节点的变更所以涉及到要移除这个节点的数据cleanData方法。

replaceWith: function() {
var arg = arguments[0];
this.domManip(arguments, function(elem) {
arg = this.parentNode;
jQuery.cleanData(getAll(this));
if (arg) {
arg.replaceChild(elem, this);
}
});
return arg && (arg.length || arg.nodeType) ? this : this.remove();
}

.replaceWith()方法,和大部分其他jQuery方法一样,返回jQuery对象,所以可以和其他方法链接使用,但是需要注意的是:对于该方法而言,该对象指向已经从 DOM 中被移除的对象,而不是指向替换用的对象。

删除目标节点

jQuery( this ).remove();

然后再插入一个新节点

parent.insertBefore( elem, next );

将匹配元素集合从DOM中删除要涉及到empty,remove,detach方法。

移除

涉及节点删除的接口jquery划分了四个分别是detach,empty,remove,unwrap因为使用的范围不同,所以功能也不同,但是总得来说都是用来清理节点的。

innerText使我们常用的文本清理方法,但是火狐下不兼容,不过会提供一个类似的方法叫textContent

ie中的innerText是需要对innerHTML的值进行

1,HTML转义 等同于XML转义,对<,&等转义字符串进行处理

2,经过HTML解释和CSS样式解释

3,之后又剔除格式信息

4,之后留下的纯本文

而FF中的textContent没有2,3,在经过了html转义后直接删除所有hmtl标签后得到的纯本文

jquery对节点清除的封装

.empty()

从DOM中移除集合中匹配元素的所有子节点,为了避免内存泄露,jquery先移除子元素的数据和时间处理函数,然后移除子元素

empty:function(){

  var elem;

  i = 0;

  for(;(elem = this[i]) ! = null;i++){

    if(elem.nodeType ===1){

      jQuery.cleanData(getAll(elem,false));

      elem.textContext = "";

    }

  }

  return this;

}

jquery是合集元素,所以我们遍历下this[i],然后直接把元素的textContent清空就可以了,但是需要注意jquery.cleanData()方法,清除节点不单单只有元素,还有附加在上面的事件处理和数据缓存

jquery.cleanData方法,就是通过元素判断上绑定的expando的这个uuid在与之对应的cache中找到数据与事件句柄加以删除

.remove

.remove()将元素移出DOM,当我们想将元素自身移除时我们用.remove,同时也会移除元素内部的一切,包括绑定事件及与该元素相关的jquery数据

remove: function(selector, keepData /* Internal Use Only */ ) {
var elem,
elems = selector ? jQuery.filter(selector, this) : this,
i = 0;
for (;
(elem = elems[i]) != null; i++) {
if (!keepData && elem.nodeType === 1) {
jQuery.cleanData(getAll(elem));
}
if (elem.parentNode) {
if (keepData && jQuery.contains(elem.ownerDocument, elem)) {
setGlobalEval(getAll(elem, "script"));
}
elem.parentNode.removeChild(elem);
}
}
return this;
}
remove是empty的加强版,把本身的父节点也清除掉了,因为remove支持过滤器所以支持selector,remove需要删除自身及其所有的子元素包括事件与数据,所以要通过找到父节点nameNode移除。 .detach()
如果你想删除元素,不破坏他们的数据或者事件处理程序,.detach方法和.remove()一样,除了.detach()保存所有jquery数据和被移走的元素相关联,当需要移走一个元素,不久又将员插入DOM时,这种方法很有用
detach: function(selector) {
return this.remove(selector, true);
}
这个方法也很简单,意味着这要暂时移除节点,但是不销毁对应的事件与数据,在remove方法中支持传递布尔值用来处理这个cleanData的过滤。

克隆

所有类型节点都会有一个方法就是cloneNode,也就是克隆节点。克隆的操作也是我们常常需要使用的,本来就是一个很简单的接口,但是还是有一些细节问题需要处理。

cloneNode不会复制javascript属性,比如事件,这个方法只会复制特性。当然IE有这个BUG它会复制事件处理程序。cloneNode(a)方法接受一个布尔值参数,表示是否深拷贝。

true:表示执行深拷贝,复制本节点以及整个子节点树。

false:浅拷贝,只复制节点本身。

复制后返回的节点副本属于文档所有,但是并没有父节点。除非使用 appendChild,insertChild(),replaceChild()将它添加到文档。

关于事件的处理

IE旧版会克隆原生的事件,所以我们需要做克隆的时候先移除掉,当然2.1.1版本是不再兼容低级版本了。所以我们考虑的是jQuery体系的处理,因为这里还没有涉及到事件的原理,所以我们暂时先初步理解下,jQuery的事件处理是非常nice的,利用了数据缓存的机制,把数据都缓存在内存中而不直接跟dom元素绑定,这样的好处很多,具体我们在事件交互那一章会超详细讲解。

cloneNode(true)的时候是遍历的节点,但是不会把对应的事件与数据给复制,但是jQuery.clone方法克隆的时候,是会把该节点上的事件与数据给一并复制过去的,这样的机制是建立在数据分离的基础上。简单来说,jQuery在DOM上做了一个uuid的标记,然后把与这个dom相关联的所有数据都放到一个内存区域,通过这个uuid映射,这样我们在深度拷贝 dom 的时候自然也可以把内存的数据给复制一份了,当然这里要注意一个问题,事件是不能被复制的,需要重新绑定了。

因为操作都是跟data_priv与data_user挂钩的所以我模拟的话实现的代码量太大了,这里就直接给大概的流程吧。

首先我们elem.cloneNode(true)直接给这个元素克隆一份,我们要做的就是把克隆后的元素加入事件与数据。

jQuery内部的数据都缓存在data_priv中,包括事件,data_user是提供给用户操作的,用户的数据。

所以就需要把这个2个缓存给找出来然后混入到新的克隆节点中,jQuery都是提供接口data_priv.access,data_priv.set。

值得注意的事件的复制是需要重新jQuery.event.add绑定的,如果节点是有嵌套的话,需要遍历每一个元素节点,在每个节点上都要处理事件与数据。