jQuery是支持链式操作的,现在我们用jQuery来实现一个导航栏:
./css/default.css:
/* reset */
body{margin:0;padding:0 0 12px 0;font-size:12px;line-height:22px;font-family:"\5b8b\4f53","Arial Narrow";background:#fff;}
form,ul,li,p,h1,h2,h3,h4,h5,h6{margin:0;padding:0;}
input,select{font-size:12px;line-height:16px;}
img{border:0;}
ul,li{list-style-type:none;}
a {color:#00007F;text-decoration:none;}
a:hover {color:#bd0a01;text-decoration:underline;}
.box {
width: 150px;
margin: 0 auto;
}
.menu{
overflow:hidden;
border-color: #C4D5DF;
border-style: solid;
border-width: 0 1px 1px;
}
/* lv1 */
.menu li.level1 a{
display: block;
height: 28px;
line-height: 28px;
background:#EBF3F8;
font-weight:700;
color: #5893B7;
text-indent: 14px;
border-top: 1px solid #C4D5DF;
}
.menu li.level1 a:hover{text-decoration:none;}
.menu li.level1 a.current{background:#B1D7EF;}
/* lv2 */
.menu li ul{overflow:hidden;}
.menu li ul.level2{display:none;}
.menu li ul.level2 li a{
display: block;
height: 28px;
line-height: 28px;
background:#ffffff;
font-weight:400;
color: #42556B;
text-indent: 18px;
border-top: 0px solid #ffffff;
overflow: hidden;
}
.menu li ul.level2 li a:hover{
color:#f60;
}
navigator.html:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>导航栏</title>
<link rel="stylesheet" href="css/default.css" type="text/css" />
<!-- 引入 jQuery -->
<script src="../../scripts/jquery.js" type="text/javascript"></script>
<script type="text/javascript">
//等待dom元素加载完毕.
$(document).ready(function () {
$(".level1 > a").click(function () {
$(this).addClass("current") //给当前元素添加"current"样式
.next().show() //下一个元素显示
.parent().siblings().children("a").removeClass("current") //父元素的兄弟元素的子元素<a>移除"current"样式
.next().hide(); //它们的下一个元素隐藏
return false;
});
});
</script>
</head>
<body>
<p>第三步:优化后:</p>
<div class="box">
<ul class="menu">
<li class="level1">
<a href="#none">衬衫</a>
<ul class="level2">
<li><a href="#none">短袖衬衫</a></li>
<li><a href="#none">长袖衬衫</a></li>
<li><a href="#none">短袖T恤</a></li>
<li><a href="#none">长袖T恤</a></li>
</ul>
</li>
<li class="level1">
<a href="#none">卫衣</a>
<ul class="level2">
<li><a href="#none">开襟卫衣</a></li>
<li><a href="#none">套头卫衣</a></li>
<li><a href="#none">运动卫衣</a></li>
<li><a href="#none">童装卫衣</a></li>
</ul>
</li>
<li class="level1">
<a href="#none">裤子</a>
<ul class="level2">
<li><a href="#none">短裤</a></li>
<li><a href="#none">休闲裤</a></li>
<li><a href="#none">牛仔裤</a></li>
<li><a href="#none">免烫卡其裤</a></li>
</ul>
</li>
</ul>
</div>
</body>
</html>
显示效果如下:
导航菜单可以正常响应,我们来分析一下事件是如何处理的吧。
- $(document)就是返回了一个jQuery特有的实例,其实就是jQuery.fn.init实例;
- ready函数上篇文章已经分析过了,就是把参数里的回调函数加入事件队列里,等处理下一个事件队列里的时候就可以执行了,这时候通常所有dom元素都可以正常访问了,当然包含一些特殊情况,比如图片可能还没有加载完成等。
- $(".level1 > a"),这个函数也好分析,就是使用css选择器来获得它对应的jquery实例,我们看一下jquery.js源码,第2430行:
// Handle HTML strings
if (typeof selector === "string") {
直接进入第1个if语句执行,接下来:
//我们的selector第一个字符是 .,所以进入else
if (selector[0] === "<" &&
selector[selector.length - 1] === ">" &&
selector.length >= 3) {
// Assume that strings that start and end with <> are HTML and skip the regex check
match = [null, selector, null];
} else {
match = rquickExpr.exec(selector);
}
看下rquickExpr.exec()做了什么,先看一下rquickExpr是个什么东东,原来是个RegExp类型的实例,也就是一个正则表达式实例,定义如下:
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/
与selector进行正则匹配,结果返回给match变量,match是init()函数里的局部变量,我们可以先自己计算出match的结果值,那就是null,好了,继续往下看:
match = null, context == undefined, 那么就会进入第1个else if语句:
else if (!context || context.jquery) {
// 最后实际调用root.find('.level1 > a');
return (context || root).find(selector);
} else {
return this.constructor(context).find(selector);
}
root等于什么呢,root参数是undefined,在第2427行赋值:
root = root || rootjQuery;
所以root等于rootjQuery,rootjQeury在第2520行赋值:
rootjQuery = jQuery(document);
好吧,这个值就是document元素对应的jQuery实例,就是$(document)。那我们接下来看一下它的find函数是怎么工作的呢?在2363行可以看到find方法,代码不多:
find: function (selector) {
var i, ret,
len = this.length,
self = this;
// if条件显示不满足,因为我们传进来的就是一个字符串,跳过这个if语句
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;
}
}
}));
}
ret = this.pushStack([]);
for (i = 0; i < len; i++) {
jQuery.find(selector, self[i], ret);
}
return len > 1 ? jQuery.uniqueSort(ret) : ret;
}
如果要看懂find代码内容,就得继续分析里面的代码了。在此之前,你会发现经常看到一些方法调用,比如this.pushStack(), 或者jQuery.find(),在1万行代码里怎么查找呢?如果你没有源码,只有一个jquery.js文件,那么教你一个方法,凡是使用jQuery.extend函数调用,参数里的方法都是加入到jQuery函数里了,可以直接使用jQuery.<functionName>()的,如果使用jQuery.fn.extend()调用,那么方法就加入到jQuery.fn.init()实例里了,可以简单理解为jQuery实例。使用extend()调用的就相当于创建了静态方法,fn.extend()调用的就相当于创建了实例方法。
另外在1万多行代码里来回跳转看,肯定会疯掉的。下面,我们直接在源码的文件夹下面查看了,这样就会方便很多。
我们接下来看一下find()方法中调用的pushStack()方法:
ret = this.pushStack([]);
在core.js第58行里有它的实现:
pushStack: function( elems ) {
// 这里constructor()是个什么东西,constructor定义在第34行,就是jQuery函数
// this.contructor(),就是创建了一个jQuery实例
var ret = jQuery.merge( this.constructor(), elems );
// Add the old object onto the stack (as a reference)
ret.prevObject = this;
// Return the newly-formed element set
return ret;
}
在第318行,看一下merge函数做了什么:
merge: function( first, second ) {
var len = +second.length,
j = 0,
i = first.length;
for ( ; j < len; j++ ) {
first[ i++ ] = second[ j ];
}
first.length = i;
return first;
}
简单,就是把second的所有属性全部添加到first的末尾,其实就是属性拷贝嘛。由于我们在调用pushStack的时候传入了一个空数组[],所以pushStack其实就是创建一个新的jQuery实例。接下来继续分析find()下面的这行:
// len = this.length, this其实是rootjQuery,它在init.js第120行进行了初始化的,在第99行
// 添加了属性"0"和length,this[0] = document, this.length = 1。所以len = 1,下面循环就执行了一次
for (i = 0; i < len; i++) {
// 相当于调用jQuery.find(".level1 > a", document, ret);// ret就是上面pushStack返回的jQuery实例
jQuery.find(selector, self[i], ret);
}
再看一下jQuery.find方法,在selector.js最下面,可以看到如下语句:
jQuery.find = find;
原来它就是这个文件里的find()函数,看一下这个函数:
// selector = ".level1 > a", context = document, results = jQuery实例, seed = undefined
function find( selector, context, results, seed ) {
var m, i, elem, nid, match, groups, newSelector,
newContext = context && context.ownerDocument,
// nodeType defaults to 9, since context defaults to document
nodeType = context ? context.nodeType : 9;
results = results || [];
// 我们的selector就是一个string,那么下面的if不执行
if ( typeof selector !== "string" || !selector ||
nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
return results;
}
// seed = undefined,满足if条件,进入if语句
if ( !seed ) {
// 下面这个函数很简单,我们相当于调用了setDocument(document),主要初始化了3个全局变量
// document = document, documentElement= html, documentIsHTML = true
setDocument( context );
context = context || document;
// if条件成立,进入if语句
if ( documentIsHTML ) {
// nodeType=1,因为是document类型,但是match是null,不执行这个if语句
if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) {
//....此处代码省略
}
// 这里if条件满足,进入if语句
if ( !nonnativeSelectorCache[ selector + " " ] &&
( !rbuggyQSA || !rbuggyQSA.test( selector ) ) ) {
newSelector = selector;
newContext = context;
// if条件不成立,跳过
if ( nodeType === 1 &&
( rdescend.test( selector ) || rcombinators.test( selector ) ) ) {
//....代码省略
}
try {
//开始执行此处,push其实就是import push from "./var/push.js",本质就是[].push方法
//也就是数组的push方法嘛,使用apply来调用,传入第一个this参数为我们之前创建的jQuery实例,
//第2个参数就是document.querySelectorAll(".level1 > a"),简单,这其实就是使用js的标准接
//口来获取所有的元素,然后把返回的NodeList元素全部添加到jQuery实例中,然后返回这个jQuery
//实例。
push.apply( results,
newContext.querySelectorAll( newSelector )
);
return results;
} catch ( qsaError ) {
nonnativeSelectorCache( selector, true );
} finally {
if ( nid === expando ) {
context.removeAttribute( "id" );
}
}
}
}
}
// All others
return select( selector.replace( rtrim, "$1" ), context, results, seed );
好吧,代码看了这么多,晕头转向,现在总结一下,$(".level1 > a") 这句话实际上就是创建一个jQuery实例,然后把查找到的原始元素都添加到这个jQuery类数组里,它的属性如下图所示:
一共找到了3个a元素,所以都加到这里面了。
接下来看一下$(".level1 > a").click()这个方法调用做了什么。在deprecated/event.js最后几行,可以看到如下代码:
jQuery.each(
( "blur focus focusin focusout resize scroll click dblclick " +
"mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " +
"change select submit keydown keypress keyup contextmenu" ).split( " " ),
function( _i, name ) {
// Handle event binding
jQuery.fn[ name ] = function( data, fn ) {
return arguments.length > 0 ?
this.on( name, null, data, fn ) :
this.trigger( name );
};
}
);
在这里把常用的事件都给加到了jQuery原型中去了,这样就变成了实例方法了,类似这样jQuery.fn[“click”]。
根据参数的不同,调用this.on或者this.trigger,这2个方法都定义在event.js或者event/trigger.js中,比较好找。
根据当前的调用,我们给click传入了一个回调函数,所以参数data = 回调函数,那么实际$(".level1 > a").click(…)就会调用this.on(“click”, null, 回调函数, undefine),接下来看一下event.js中的on方法的实现:
jQuery.fn.extend( {
on: function( types, selector, data, fn ) {
return on( this, types, selector, data, fn );
},
继续看同文件中的on函数:
// elem就是我们的jQuery实例$(".level1 > a"), types="click", selector=null, data=我们传入的回调函数
// fn=undefined, one=undefined
function on( elem, types, selector, data, fn, one ) {
var origFn, type;
// 我们的types是字符串,不是object,跳过
if ( typeof types === "object" ) {
//省略代码
}
// if不成立,跳过
if ( data == null && fn == null ) {
// ( types, fn )
fn = selector;
data = selector = undefined;
} else if ( fn == null ) {//条件成立,进入
if ( typeof selector === "string" ) {//不成立,不进入if
// ( types, selector, fn )
fn = data;
data = undefined;
} else {
// 这个地方交换一下参数,现在fn=我们的回调函数,data=null
fn = data;
data = selector;
selector = undefined;
}
}
//这里的if,else if都不成立,都不执行
if ( fn === false ) {
fn = returnFalse;
} else if ( !fn ) {
return elem;
}
// one = undefined,不成立,跳过
if ( one === 1 ) {
//省略代码
}
//执行我们jQuery实例的each方法
return elem.each( function() {
jQuery.event.add( this, types, fn, data, selector );
} );
}
上面的代码执行了elem.each方法,那么我们看看这个each方法是怎么执行的,在core.js里有定义:
// Execute a callback for every element in the matched set.
each: function( callback ) {
return jQuery.each( this, callback );
},
...
...
//jQuery.each还在当前文件中可以找到,如下所示
// obj = this,我们的jQuery实例$(".level1 > a"),它的length是3,索引0到2分别保存了搜索到的原始的a元素
// 下面这个方法就是把每个a元素绑定到callback的this参数
each: function( obj, callback ) {
var length, i = 0;
if ( isArrayLike( obj ) ) {
length = obj.length;
for ( ; i < length; i++ ) {
if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
break;
}
}
} else {
for ( i in obj ) {
if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
break;
}
}
}
return obj;
},
现在回到上面讲到的on方法的最后一行:
//执行我们jQuery实例的each方法
// 这个回调一共调用了3次,相当于这样调用
// jQuery.event.add(a/*第0个a元素*/, "click", 我们的回调函数, null, undefined);
//jQuery.event.add(a/*第1个a元素*/, "click", 我们的回调函数, null, undefined);
//jQuery.event.add(a/*第2个a元素*/, "click", 我们的回调函数, null, undefined);
return elem.each( function() {
jQuery.event.add( this, types, fn, data, selector );
} );
再看一下event.add方法是怎样运行的。还是在当前的event.js文件中:
//我删除了很多代码,只看主要的吧
jQuery.event = {
add: function( elem, types, handler, data, selector ) {
//.....
if ( !( eventHandle = elemData.handle ) ) {
//这是真正执行处理事件的地方,原来的回调已经被存起来了
eventHandle = elemData.handle = function( e ) {
// 比如用户点击菜单时,执行这个回调函数,其中我们的回调中的this绑定为了
// 当前元素,也就是你当前点击的元素a
return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
jQuery.event.dispatch.apply( elem, arguments ) : undefined;
};
}
//.....
while ( t-- ) {
// Only use addEventListener if the special events handler returns false
if ( !special.setup ||
special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
// jQuery包装的回调函数通过addEventListener添加到元素上了
if ( elem.addEventListener ) {
elem.addEventListener( type, eventHandle );
}
}
}
}
},
当用户点击了导航栏时,我们看看回调函数里的语句是怎么执行的:
$(this).addClass("current") //给当前元素添加"current"样式
.next().show() //下一个元素显示
.parent().siblings().children("a").removeClass("current") //父元素的兄弟元素的子元素<a>移除"current"样式
.next().hide(); //它们的下一个元素隐藏
return false; //返回false,表示阻止默认处理,比如a元素,不会发生跳转
$(this)执行会执行init.js第98行,创建一个当前元素a的jQuery实例,这样他就可以使用jQuery实例方法了:
// HANDLE: $(DOMElement)
} else if ( selector.nodeType ) {
this[ 0 ] = selector;
this.length = 1;
return this;
}
接着调用addClass()方法给元素添加css类,在attributes/classes.js中可以看到它的定义:
jQuery.fn.extend( {
addClass: function( value ) {
var classes, elem, cur, curValue, clazz, j, finalValue,
i = 0;
if ( typeof value === "function" ) {
return this.each( function( j ) {
jQuery( this ).addClass( value.call( this, j, getClass( this ) ) );
} );
}
classes = classesToArray( value );
if ( classes.length ) {
while ( ( elem = this[ i++ ] ) ) {
curValue = getClass( elem );
cur = elem.nodeType === 1 && ( " " + stripAndCollapse( curValue ) + " " );
if ( cur ) {
j = 0;
while ( ( clazz = classes[ j++ ] ) ) {
if ( cur.indexOf( " " + clazz + " " ) < 0 ) {
cur += clazz + " ";
}
}
// Only assign if different to avoid unneeded rendering.
finalValue = stripAndCollapse( cur );
if ( curValue !== finalValue ) {
elem.setAttribute( "class", finalValue );
}
}
}
}
return this;
},
很简单,就是通过setAttribute把这个class类加到入元素的class属性中。最后返回this,也就是元素的jQuery实例本身,这是为了方便链式调用。接下来调用next()方法,在traversing.js第120行可以找到定义:
function sibling( cur, dir ) {
while ( ( cur = cur[ dir ] ) && cur.nodeType !== 1 ) {}
return cur;
}
//....
next: function( elem ) {
return sibling( elem, "nextSibling" );
},
原来就是调用元素的"nextSibling"属性,这是标准属性,直接返回下一个兄弟结点,也就是html中紧接着这个导航元素a的ul元素,这个ul元素的class是"level2",当前最后返回的就是这个ul元素对应的jQuery实例。接着调用这个jQuery实例的show()方法,其实就是设置这个元素的display属性。后面的链式调用流程都差不多,就不细说了。
jQuery对象和DOM对象的相互转换
前面我们已经知道了,每个jQuery实例都是一个类数组,原始的元素就存放在索引0时,如果是多个元素,那么会依次存放在索引0…length-1上。
例如:
const $a = $(".level1 > a");// jQuery实例,代表3个a元素
//那么我们可以依次获取这3个DOM元素a
const a1 = $a[0],a2 = $a[1],a3 = $a[2];
假如当前我们正在使用DOM元素a1,如果要把它转回jQuery对象,很简单:
const $a = $(a1);
处理和其它库的命名冲突
我们为了使用简单,一般使用$来代表jQuery函数调用,当然如果你同时引用了别的第3方库的话,记住要把jQuery.js引入放到其它3方库的后面,比如:
<!-- 引入 prototype -->
<script src="lib/prototype.js" type="text/javascript"></script>
<!-- 引入 jQuery -->
<script src="../../scripts/jquery.js" type="text/javascript"></script>
<script type="text/javascript">
jQuery.noConflict();
</script>
调用noConflict()方法就可以解决冲突,这是为什么呢?例如上面的prototype.js也使用全局名字$,那我们看看noConflict()是怎么解决这个命名冲突的,查看exports/global.js :
var
// 看下面这2行代码,jQuery覆盖同名的变量时,提前把可能第3库的名字写到了
// _jQuery和_$变量里,这样如果在调用noConflict时,就可以把window.$恢复成原来的
// $
// Map over jQuery in case of overwrite
_jQuery = window.jQuery,
// Map over the $ in case of overwrite
_$ = window.$;
jQuery.noConflict = function( deep ) {
if ( window.$ === jQuery ) {
window.$ = _$;
}
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery;
}
return jQuery;
};
// Expose jQuery and $ identifiers, even in AMD
// (#7102#comment:10, https://github.com/jquery/jquery/pull/557)
// and CommonJS for browser emulators (#13566)
if ( typeof noGlobal === "undefined" ) {
window.jQuery = window.$ = jQuery;
}
解决冲突之后,就不能再使用$来代表jQuery了,你可以直接使用jQuery,自己创建一个别名指向jQuery也可以。