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 ...
随机推荐
- Android学习之listview的下拉刷新、上拉载入
本例是在上例的基础上完成的.本例实现的listview上拉载入.下拉刷新功能,是在开源网站上别人写好的listview,主要是对listview的控件进行重写,添加了footer和header. 1. ...
- Hadoop 安装(3) JDK 的安装
使用vsftp上传,JDK 和 Hadoop 到 Hadoop 用户目录. 安装JDK root 身份登录Slave4.Hadoop,在 /usr 下创建 "Java"文件夹,将j ...
- android开发字符串工具类(一)
package com.gzcivil.utils; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; impo ...
- Free Sql Server SMSS format Plugin
免费Sql Server 格式化插件 http://www.apexsql.com/sql_tools_refactor.aspx http://architectshack.com/PoorMans ...
- 屏蔽错误:LNK2038
最近在使用Qt(VS2010编译)的过程中,需要调用COM库,在Qt中加入了QAxContainer模块,是一个LIB库,在把编译模式从Debug改为Release 后链接报告了一堆错误 -1: 错误 ...
- TCP/IP详解之:TCP
第17章 TCP:传输控制协议 TCP提供了一种可靠的面向连接的字节流运输层服务 TCP的服务 尽管TCP和UDP都使用相同的网络层(IP),TCP却向应用层提供与UDP完全不同的服务. TCP通过下 ...
- android中viewPager实现的屏幕左右切换(入门篇)
大多数的APP都可以实现几个屏幕来回的切换, 首先新建两个Activity,内容随意,布局随意.接下来在MainActivity.xml: <RelativeLayout xmlns:andro ...
- CSS垂直居中解决方案
问题场景 应用的地方比较普遍,这里有两个赤裸裸的栗子: 也有很多流行的方案,这里只针对各种方案的适用场景来做一些分析 问题抽象 其实,垂直居中问题可以简化成这样:一个容器HTML元素(#conta ...
- requireJS到底是什么?
1.requireJS是让js代码模块化:而且js之间的依赖关系,不再依靠script标签的顺序,可以加载不阻塞 2.requireJS加载js的方法:<script data-main=&qu ...
- JQuery DataTables Editor---只修改页面内容
近来在工作中需要对JQuery DataTables进行增,删,改的操作,在网上找了一些资料,感觉比较的好的就是(http://editor.datatables.net/)文章中所展示的操作方法(如 ...