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的时候做了什么.
find: function( selector ) {
var i,
len = this.length,
ret = [],
self = this; if ( typeof selector !== "string" ) {
return this.pushStack( jQuery( selector ).filter(function() {
for ( i = 0; i < len; i++ ) {
if ( jQuery.contains( self[ i ], this ) ) {
return true;
}
}
}) );
} for ( i = 0; i < len; i++ ) {
jQuery.find( selector, self[ i ], ret );
} // Needed because $( selector, context ) becomes $( context ).find( selector )
ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret );
ret.selector = this.selector ? this.selector + " " + selector : selector;
return ret;
}
find
jQuery.each({
parent: function( elem ) {
var parent = elem.parentNode;
return parent && parent.nodeType !== 11 ? parent : null;
},
parents: function( elem ) {
return jQuery.dir( elem, "parentNode" );
},
parentsUntil: function( elem, i, until ) {
return jQuery.dir( elem, "parentNode", until );
},
next: function( elem ) {
return sibling( elem, "nextSibling" );
},
prev: function( elem ) {
return sibling( elem, "previousSibling" );
},
nextAll: function( elem ) {
return jQuery.dir( elem, "nextSibling" );
},
prevAll: function( elem ) {
return jQuery.dir( elem, "previousSibling" );
},
nextUntil: function( elem, i, until ) {
return jQuery.dir( elem, "nextSibling", until );
},
prevUntil: function( elem, i, until ) {
return jQuery.dir( elem, "previousSibling", until );
},
siblings: function( elem ) {
return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
},
children: function( elem ) {
return jQuery.sibling( elem.firstChild );
},
contents: function( elem ) {
return elem.contentDocument || jQuery.merge( [], elem.childNodes );
}
}, function( name, fn ) {
jQuery.fn[ name ] = function( until, selector ) {
var matched = jQuery.map( this, fn, until ); if ( name.slice( -5 ) !== "Until" ) {
selector = until;
} if ( selector && typeof selector === "string" ) {
matched = jQuery.filter( selector, matched );
} if ( this.length > 1 ) {
// Remove duplicates
if ( !guaranteedUnique[ name ] ) {
jQuery.unique( matched );
} // Reverse order for parents* and prev-derivatives
if ( rparentsprev.test( name ) ) {
matched.reverse();
}
} return this.pushStack( matched );
};
});
traversing
这两种函数都在最后调用了this.pushStack
pushStack: function( elems ) { // Build a new jQuery matched element set
var ret = jQuery.merge( this.constructor(), elems ); // Add the old object onto the stack (as a reference)
ret.prevObject = this;
ret.context = this.context; // Return the newly-formed element set
return ret;
}
这个函数在第7行中将this赋值给了新对象的prevObject属性, 也就是说, 我们在每次通过已有的jQuery对象调用find或者children, parent...进行查找的时候都会把原来的保存在新对象中, 这样就提供了一个可回退的查找栈.
那么当我们使用$("#some-list").children("li").find("a")这种方式进行查找的时候, 可以从后面的结果中回溯到上一次查找的结果, 演示示例.
基本遍历
jQuery的遍历思路很简单. 它先提供了两个基本的遍历函数, 一个是dir, 一个是sibling , 然后创建快捷的遍历方法调用基本遍历函数, 再经过后续的去重封装成jQuery, 最后压栈返回结果.(q君: 信息量好大, 看完下面详细解说再看这个流程就好懂了 )
dir: function( elem, dir, until ) {
var matched = [],
truncate = until !== undefined; while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) {
if ( elem.nodeType === 1 ) {
if ( truncate && jQuery( elem ).is( until ) ) {
break;
}
matched.push( elem );
}
}
return matched;
}, sibling: function( n, elem ) {
var matched = []; for ( ; n; n = n.nextSibling ) {
if ( n.nodeType === 1 && n !== elem ) {
matched.push( n );
}
} return matched;
}
基本遍历: 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的对象或者没找到, 并返回这个对象.
function sibling( cur, dir ) {
while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {}
return cur;
}
快捷方式
jQuery提供 parent , parents , next 等十几种遍历方法, 这些方法都是以三个基本遍历方法为基础实现, 这段代码看起来非常优雅, 我忍不住要再贴一遍, 虽然上面已经有了.
jQuery.each({
parent: function( elem ) {
var parent = elem.parentNode;
return parent && parent.nodeType !== 11 ? parent : null;
},
parents: function( elem ) {
return jQuery.dir( elem, "parentNode" );
},
parentsUntil: function( elem, i, until ) {
return jQuery.dir( elem, "parentNode", until );
},
next: function( elem ) {
return sibling( elem, "nextSibling" );
},
prev: function( elem ) {
return sibling( elem, "previousSibling" );
},
nextAll: function( elem ) {
return jQuery.dir( elem, "nextSibling" );
},
prevAll: function( elem ) {
return jQuery.dir( elem, "previousSibling" );
},
nextUntil: function( elem, i, until ) {
return jQuery.dir( elem, "nextSibling", until );
},
prevUntil: function( elem, i, until ) {
return jQuery.dir( elem, "previousSibling", until );
},
siblings: function( elem ) {
return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem );
},
children: function( elem ) {
return jQuery.sibling( elem.firstChild );
},
contents: function( elem ) {
return elem.contentDocument || jQuery.merge( [], elem.childNodes );
}
}, function( name, fn ) {
jQuery.fn[ name ] = function( until, selector ) {
var matched = jQuery.map( this, fn, until ); if ( name.slice( -5 ) !== "Until" ) {
selector = until;
} if ( selector && typeof selector === "string" ) {
matched = jQuery.filter( selector, matched );
} if ( this.length > 1 ) {
// Remove duplicates
if ( !guaranteedUnique[ name ] ) {
jQuery.unique( matched );
} // Reverse order for parents* and prev-derivatives
if ( rparentsprev.test( name ) ) {
matched.reverse();
}
} return this.pushStack( matched );
};
});
当我第一次看见这段代码的时候, 不禁感叹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吧!
jQuery 源码分析和使用心得 - 文档遍历 ( traversing.js )的更多相关文章
- jQuery 源码分析和使用心得 - 关于源码
说到jQuery, 大家可能直觉的认为jQuery的源码应该就是一个jquery.xx.js这样的一个文件. 但是看到真正的源码的时候, 整个人都思密达了.jQuery的源码做的事远比你想象的多, 为 ...
- jQuery 源码分析和使用心得 - 序
众所周知, jQuery (个人简称为jq) 在前端开发中占有着非常重要的地位, 可以说jQuery的存在大大降低了学习网页设计和交互的门槛, 他的简单的语法和顺畅的使用逻辑激发了人们强烈的学习兴趣, ...
- jQuery 源码分析和使用心得 - core.js
core是jQuery的核心内容, 包含了最基础的方法, 比如我们常用的 $(selector, context), 用于遍历操作的 each, map, eq, first 识别变量类型的 isAr ...
- [转] jQuery源码分析-如何做jQuery源码分析
jQuery源码分析系列(持续更新) jQuery的源码有些晦涩难懂,本文分享一些我看源码的方法,每一个模块我基本按照这样的顺序去学习. 当我读到难度的书或者源码时,会和<如何阅读一本书> ...
- jQuery源码分析-each函数
本文部分截取自且行且思 jQuery.each方法用于遍历一个数组或对象,并对当前遍历的元素进行处理,在jQuery使用的频率非常大,下面就这个函数做了详细讲解: 复制代码代码 /*! * jQuer ...
- jQuery源码分析系列
声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...
- jQuery 源码分析 8: 回头看jQuery的构造器(jQuery.fn,jQury.prototype,jQuery.fn.init.prototype的分析)
在第一篇jQuery源码分析中,简单分析了jQuery对象的构造过程,里面提到了jQuery.fn.jQuery.prototype.jQuery.fn.init.prototype的关系. 从代码中 ...
- [转]jQuery源码分析系列
文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaro ...
- jquery源码分析之一前言篇
1.问:jquery源码分析的版本是什么? 答:v3.2.1 2.问:为什么要分析jquery源码? 答:javascript是一切js框架的基础,jquery.es6.vue.angular.rea ...
随机推荐
- Js闭包的用途
本来想总结一点JavaScript中的闭包的一些用法,在查资料的时候发现了一篇很好的文章,就转过来收藏了,下面附上传送门: js闭包的用途 ---------sunlylorn 我们来看看闭包的用途. ...
- BFC块级排版上下文
1.BFC 全称是块级排版上下文,用于对块级元素排版,默认情况下只有根元素(body)一个块级上下文,但是如果一个块级元素 设置了float:left,overflow:hidden或position ...
- C# for循环①护栏长度 ②广场砖面积 ③判断闰年平年
// static void Main(string[] args) { const double PI = 3.14; const int BAR_U ...
- STL_函数模板
#include <iostream>#include <string>using namespace std; #define MAX(T) \ T max_##T (T x ...
- python正则表达式练习篇
练习一: 利用who命令输出所有已经登录系统的用户的信息,并把登录名.用户登录时的电传.登录时间.登录地址利用正则表达式分割开来. 数据的格式: %who wesc console Jun 20 20 ...
- 做了一个jquery插件,使表格的标题列可左右拉伸
示例下载 插件名称命名为:jquery.tableresize.js,代码如下: /* Writen by mlcactus, 2014-11-24 这是我封装的一个jquery插件,能够使table ...
- PHP 数组和对象的相互转化
对象和数组的相互转化在开发中也是很常见,一般不是多维的情况下直接(array)和(object)就可搞定了,多维的话,遍历下也就可以了: 1 <?php 2 class test 3 { 4 p ...
- 线程-run和start
import java.lang.Thread; class Machine extends Thread{ public void run() { int a ; for( a = 0 ; a &l ...
- C Traps and Pitfallss
第一章 词法“陷阱” 发送阿罡发公司阿发送个发送阿罡发公司阿发送个 第二章
- CC EAL认证
国际通用准则(CC) CC(Common Criteria)是国际标准化组织统一现有多种准则的结果,是目前最全面的评价准则.1996年6月,CC第一版发布:1998年5月,CC第二版发布:1999年 ...