jQuery 源码分析和使用心得 - 文档遍历 ( traversing.js )

  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吧!

  

时间: 2024-11-12 08:26:47

jQuery 源码分析和使用心得 - 文档遍历 ( traversing.js )的相关文章

jQuery 源码分析和使用心得 - 关于源码

说到jQuery, 大家可能直觉的认为jQuery的源码应该就是一个jquery.xx.js这样的一个文件. 但是看到真正的源码的时候, 整个人都思密达了.jQuery的源码做的事远比你想象的多, 为了保证代码的可维护性, 健壮性, 通用性等等, jQuery将各个功能模块独立成单个的js文件, 并通过依赖管理管理模块之间的依赖关系, 在构建的时候通过这种依赖关系将各个模块合并成一个js文件, 最后经过压缩混淆等步骤才会产生我们经常用的jquery.xx.min.js文件( q君:上来就说这些恶

jQuery 源码分析和使用心得 - 序

众所周知, jQuery (个人简称为jq) 在前端开发中占有着非常重要的地位, 可以说jQuery的存在大大降低了学习网页设计和交互的门槛, 他的简单的语法和顺畅的使用逻辑激发了人们强烈的学习兴趣, 甚至出现了"jq狗"一类只会jQuery而不会传统dom的存在. 所以, 我也很想来伸一脚, 搅一搅这块已经快烂了的浑水. 不过, jQuery虽简单, 但是怎么把jQuery用好, 用的恰当, 还是需要一点点时间来学习和总结经验的. 所以呢, 我就希望可以通过对jQuery源码的分析,

jQuery 源码分析和使用心得 - core.js

core是jQuery的核心内容, 包含了最基础的方法, 比如我们常用的 $(selector, context), 用于遍历操作的 each, map, eq, first 识别变量类型的 isArray, isNumeric, type 等 . 这些方法为jQuery后续操作提供最基础的支持. 构造函数 jQuery( selector, context ) 说到jQuery, 大家可能最熟悉的就是 $(selector, context) , 我觉得这也是jQuery受到人们欢迎的很大一部

[转] jQuery源码分析-如何做jQuery源码分析

jQuery源码分析系列(持续更新) jQuery的源码有些晦涩难懂,本文分享一些我看源码的方法,每一个模块我基本按照这样的顺序去学习. 当我读到难度的书或者源码时,会和<如何阅读一本书>结合起来进行学习.推荐读读这本书,你可以从这里和这里下载. 第一部分:检视阅读 1. 收集参考资料:官方文档.书籍.百度/谷歌,专题/博客等,快速的浏览,对涉及的知识点.范围.深度.是否有参考意义等有大致的了解和判断,知道这些文章的作者想要解释或解决什么问题. 第二部分:分析阅读 2. 细读官方文档,官方有非

Jquery源码分析

1.概述 jQuery是一个非常优秀的Js库,与prototype,YUI,Mootools等众多的Js类库相比,它剑走偏锋,从web开发最实用的角度出发,抛除了一些中看但不实用的东西,为开发者提供一个短小精悍的类库.由于其个短小精悍,使用简单方便,性能相对高效.众多的开发者都选择Jquery来进行辅助的web开发. 在使用jquery时开发,我们也会时常碰到许多的问题,但是jquery的代码很晦涩,难起看懂,当开发时出现了问题,看不懂源码,不知道如何去排错. John Resig,Jquery

jQuery源码分析1

写在开头: 昨天开始,我决定要认真的看看jQuery的源码,选择1.7.2,源于公司用的这个版本.由于源码比较长,这将会是一个比较持久的过程,我将要利用业余时间,和偶尔上班不算忙的时间来进行.其实原本是打算对着源码抄一遍,将对其的理解写成注释,这也算是在强行堆代码量了吧(我想我这是有多懒,必须要反省).不过鉴于自己平时比较懒惰的可耻行径,和太多的东西都写在一起有点庞大,我想我还是有必要写成一个专栏,来记录这个过程.其实最根本的原因是:源码里都是有注释的,而且注释写得那么详尽,翻译过来就是了,但是

jQuery源码分析-each函数

本文部分截取自且行且思 jQuery.each方法用于遍历一个数组或对象,并对当前遍历的元素进行处理,在jQuery使用的频率非常大,下面就这个函数做了详细讲解: 复制代码代码 /*! * jQuery源码分析-each函数 * jQuery版本:1.4.2 * * ---------------------------------------------------------- * 函数介绍 * * each函数通过jQuery.extend函数附加到jQuery对象中: * jQuery.

jQuery源码分析系列(33) : AJAX中的前置过滤器和请求分发器

jQuery1.5以后,AJAX模块提供了三个新的方法用于管理.扩展AJAX请求,分别是: 1.前置过滤器 jQuery. ajaxPrefilter 2.请求分发器 jQuery. ajaxTransport, 3.类型转换器 ajaxConvert 源码结构: jQuery.extend({ /** * 前置过滤器 * @type {[type]} */ ajaxPrefilter: addToPrefiltersOrTransports(prefilters), /** * 请求分发器 *

jquery源码分析(二)——结构

再来复习下整体架构: jQuery源码分析(基于 jQuery 1.11 版本,共计8829行源码) (21,94)                定义了一些变量和函数jQuery=function(){} (96,280)        给jQuery添加一些方法和属性,jQuery.fn=jQuery.prototype(285,347)        extend:        jQuery的一些继承方法        更容易进行后续的扩展