jQuery之所以这么好用, 首先一点就是$()方法和它强大的选择器. 其中选择器使用的是sizzle引擎, sizzle是jQuery的子项目, 提供高效的选择器查询. 有个好消息告诉大家, 就是sizzle可以独立使用, 如果你觉得jQuery太大但又非常喜欢它的选择器, 那不妨可以用sizzle. 感兴趣的话可以到官方网站了解.
本系列内部不准备解析sizzle的源码, 一是sizzle内容相对独立, 二是内容主要涉及算法, 与整体的代码设计关系不大, 三嘛, 我的实力有限, 遇到算法就退缩了,哈哈! ( q君: 这才是主要原因吧! ). 不过也许以后技术水平到了, 出一个sizzle解析专题也未可知啊!
回过神来, 看我们的标题就知道了, jQuery这么强大, 它的众多方便的遍历方法也是一大功臣啊 .
jQuery提供了十几种方便的链式遍历方法, 让我们可以在繁杂的dom结构中自由游走, 这一章里我们就来一探究竟, 看看里面到底蕴含了怎么样奇妙的实现原理呢!
预热
DOM树
要说遍历, 首先要介绍"树" ,一些没有看过数据结构或者不了解html dom结构的人可能对树没有什么概念, 如果你已经知道了, 就跳过本段吧. 我简单的说明一下, 具体定义和非常正规的说明我就不说了,相信度娘一定可以满足你的. 我们先来想象一下一颗树, 他有根, 然后分叉出大的枝干, 然后就分出小树枝 ... 最后到叶子结束. 如果我们把根, 枝干, 小树枝, 叶子 抽象成节点, 他们之间存在连接, 这些节点和连接就组成了树. 应用到html中就是如下(来自百度图片搜索)
树根就是document, 到html元素, 然后分叉 ... 一直到最后的文本. 所以说html整个就是一颗树. 树中的所有节点都直接或间接的连通, 而且可以看到属性结构不存在环状的连接.
树的遍历就是通过document和下面的所有节点, 通过他们的连接在各个节点上游走, 访问上面的数据.
jQuery比较常用的几种遍历文档的方法有 parent parents children siblings next prev等等.
DOM属性
在解析遍历源码之前, 还要普及几点dom的几个属性和方法. 一般我们通过document.getElementByXX的这种方法就可获得dom节点和dom节点的数组. dom中包含了非常多的属性, 包括父节点, 子节点 , 相邻节点的引用, 自身的一些数值或者位置, 大小等信息. jQuery的遍历方法也是基于这些属性实现的.
有一点需要介绍的是 nodeType属性, nodeType标记了当前节点的类型. dom节点比较重要的几个是(来自百度百科)
元素节点 |
节点类型取值(nodeType) |
---|---|
元素element |
1 |
属性attr |
2 |
文本text |
3 |
注释comments |
8 |
文档document |
9 |
jQuery的"栈"
jQuery的链式查找是非常舒服的, 比如查找某个列表下的链接可以用 $("#some-list").children("li").find("a"), 这里我为什么要用多次查找呢, 因为跟jQuery的"栈"有关嘛. 我们先来看看执行children和find的时候做了什么.
1 find: function( selector ) { 2 var i, 3 len = this.length, 4 ret = [], 5 self = this; 6 7 if ( typeof selector !== "string" ) { 8 return this.pushStack( jQuery( selector ).filter(function() { 9 for ( i = 0; i < len; i++ ) { 10 if ( jQuery.contains( self[ i ], this ) ) { 11 return true; 12 } 13 } 14 }) ); 15 } 16 17 for ( i = 0; i < len; i++ ) { 18 jQuery.find( selector, self[ i ], ret ); 19 } 20 21 // Needed because $( selector, context ) becomes $( context ).find( selector ) 22 ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); 23 ret.selector = this.selector ? this.selector + " " + selector : selector; 24 return ret; 25 }
find
1 jQuery.each({ 2 parent: function( elem ) { 3 var parent = elem.parentNode; 4 return parent && parent.nodeType !== 11 ? parent : null; 5 }, 6 parents: function( elem ) { 7 return jQuery.dir( elem, "parentNode" ); 8 }, 9 parentsUntil: function( elem, i, until ) { 10 return jQuery.dir( elem, "parentNode", until ); 11 }, 12 next: function( elem ) { 13 return sibling( elem, "nextSibling" ); 14 }, 15 prev: function( elem ) { 16 return sibling( elem, "previousSibling" ); 17 }, 18 nextAll: function( elem ) { 19 return jQuery.dir( elem, "nextSibling" ); 20 }, 21 prevAll: function( elem ) { 22 return jQuery.dir( elem, "previousSibling" ); 23 }, 24 nextUntil: function( elem, i, until ) { 25 return jQuery.dir( elem, "nextSibling", until ); 26 }, 27 prevUntil: function( elem, i, until ) { 28 return jQuery.dir( elem, "previousSibling", until ); 29 }, 30 siblings: function( elem ) { 31 return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); 32 }, 33 children: function( elem ) { 34 return jQuery.sibling( elem.firstChild ); 35 }, 36 contents: function( elem ) { 37 return elem.contentDocument || jQuery.merge( [], elem.childNodes ); 38 } 39 }, function( name, fn ) { 40 jQuery.fn[ name ] = function( until, selector ) { 41 var matched = jQuery.map( this, fn, until ); 42 43 if ( name.slice( -5 ) !== "Until" ) { 44 selector = until; 45 } 46 47 if ( selector && typeof selector === "string" ) { 48 matched = jQuery.filter( selector, matched ); 49 } 50 51 if ( this.length > 1 ) { 52 // Remove duplicates 53 if ( !guaranteedUnique[ name ] ) { 54 jQuery.unique( matched ); 55 } 56 57 // Reverse order for parents* and prev-derivatives 58 if ( rparentsprev.test( name ) ) { 59 matched.reverse(); 60 } 61 } 62 63 return this.pushStack( matched ); 64 }; 65 });
traversing
这两种函数都在最后调用了this.pushStack
1 pushStack: function( elems ) { 2 3 // Build a new jQuery matched element set 4 var ret = jQuery.merge( this.constructor(), elems ); 5 6 // Add the old object onto the stack (as a reference) 7 ret.prevObject = this; 8 ret.context = this.context; 9 10 // Return the newly-formed element set 11 return ret; 12 }
这个函数在第7行中将this赋值给了新对象的prevObject属性, 也就是说, 我们在每次通过已有的jQuery对象调用find或者children, parent...进行查找的时候都会把原来的保存在新对象中, 这样就提供了一个可回退的查找栈.
那么当我们使用$("#some-list").children("li").find("a")这种方式进行查找的时候, 可以从后面的结果中回溯到上一次查找的结果, 演示示例.
基本遍历
jQuery的遍历思路很简单. 它先提供了两个基本的遍历函数, 一个是dir, 一个是sibling , 然后创建快捷的遍历方法调用基本遍历函数, 再经过后续的去重封装成jQuery, 最后压栈返回结果.(q君: 信息量好大, 看完下面详细解说再看这个流程就好懂了 )
1 dir: function( elem, dir, until ) { 2 var matched = [], 3 truncate = until !== undefined; 4 5 while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) { 6 if ( elem.nodeType === 1 ) { 7 if ( truncate && jQuery( elem ).is( until ) ) { 8 break; 9 } 10 matched.push( elem ); 11 } 12 } 13 return matched; 14 }, 15 16 sibling: function( n, elem ) { 17 var matched = []; 18 19 for ( ; n; n = n.nextSibling ) { 20 if ( n.nodeType === 1 && n !== elem ) { 21 matched.push( n ); 22 } 23 } 24 25 return matched; 26 }
基本遍历: dir sibling
dir有三个参数, function( elem, dir, until ), elem是dom对象, dir是需要遍历的属性, until是截至条件. 运行过程是循环查找elem的dir的属性, 直到没有后续元素 或者找到了document根节点(elem.nodeType !== 9) , 将所有查找到的元素放到数组中返回 .
sibling有两个参数, function( n, elem ) , n是起始dom对象, elem是结束dom对象. 它通过不断寻找nextSibling, 直到找到非element的对象(n.nodeType === 1) 或者找到了elem为止, 将所有查找到的元素放到数组中返回 .
另外还有一个基本遍历方法sibling, 这个方法并没有对外公开. 它查找dir属性直到遇到第一个element的对象或者没找到, 并返回这个对象.
1 function sibling( cur, dir ) { 2 while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {} 3 return cur; 4 }
快捷方式
jQuery提供 parent , parents , next 等十几种遍历方法, 这些方法都是以三个基本遍历方法为基础实现, 这段代码看起来非常优雅, 我忍不住要再贴一遍, 虽然上面已经有了.
1 jQuery.each({ 2 parent: function( elem ) { 3 var parent = elem.parentNode; 4 return parent && parent.nodeType !== 11 ? parent : null; 5 }, 6 parents: function( elem ) { 7 return jQuery.dir( elem, "parentNode" ); 8 }, 9 parentsUntil: function( elem, i, until ) { 10 return jQuery.dir( elem, "parentNode", until ); 11 }, 12 next: function( elem ) { 13 return sibling( elem, "nextSibling" ); 14 }, 15 prev: function( elem ) { 16 return sibling( elem, "previousSibling" ); 17 }, 18 nextAll: function( elem ) { 19 return jQuery.dir( elem, "nextSibling" ); 20 }, 21 prevAll: function( elem ) { 22 return jQuery.dir( elem, "previousSibling" ); 23 }, 24 nextUntil: function( elem, i, until ) { 25 return jQuery.dir( elem, "nextSibling", until ); 26 }, 27 prevUntil: function( elem, i, until ) { 28 return jQuery.dir( elem, "previousSibling", until ); 29 }, 30 siblings: function( elem ) { 31 return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); 32 }, 33 children: function( elem ) { 34 return jQuery.sibling( elem.firstChild ); 35 }, 36 contents: function( elem ) { 37 return elem.contentDocument || jQuery.merge( [], elem.childNodes ); 38 } 39 }, function( name, fn ) { 40 jQuery.fn[ name ] = function( until, selector ) { 41 var matched = jQuery.map( this, fn, until ); 42 43 if ( name.slice( -5 ) !== "Until" ) { 44 selector = until; 45 } 46 47 if ( selector && typeof selector === "string" ) { 48 matched = jQuery.filter( selector, matched ); 49 } 50 51 if ( this.length > 1 ) { 52 // Remove duplicates 53 if ( !guaranteedUnique[ name ] ) { 54 jQuery.unique( matched ); 55 } 56 57 // Reverse order for parents* and prev-derivatives 58 if ( rparentsprev.test( name ) ) { 59 matched.reverse(); 60 } 61 } 62 63 return this.pushStack( matched ); 64 }; 65 });
当我第一次看见这段代码的时候, 不禁感叹js真是太灵活了, 而jQuery的开发者将这种灵活性发挥的淋漓尽致. 整段代码前半部分看起来就像是一个配置. 函数名, 后面是方法的实现. 比如parents, 他调用dir方法, 传入当前elem和遍历属性"parentNode", 这个方法就会不断访问元素的parentNode属性查找父级元素, 一直查到 document位置, 返回的就是当前元素的所有父级元素.
再看后半部分, jQuery.map( this, fn, until ) 遍历本身, 对每一个元素执行fn方法, 传入until参数. 返回的就是所有遍历后得到的元素(dom元素, 可能会有重复). jQuery.filter( selector, matched )对元素进行过滤, 然后去重, 如果是parent, prev等方法, 就将结果反转顺序, 最后压栈返回.
使用建议
1. 通过prevObject可以获取上一次查找结果
2. 先提供基本方法, 然后创建快捷方式的做法可以在以后的代码中借鉴
3. 感叹jQuery吧!