Jquery源码解析(二)
今天我们带来jquery源码分析第二期,可能是最后一期哟,具体看以后的发现吧,话不多说直接上代码。
1.节点查询
先看一个函数
jquery.extend({
map: function( elems, callback, arg ) {
var length, value,
i = 0,
ret = [];
//判断是否是类数组对象,dom节点是一种类数组对象
if ( isArrayLike( elems ) ) {
length = elems.length;
for ( ; i < length; i++ ) {
value = callback( elems[ i ], i, arg );
if ( value != null ) {
ret.push( value );
}
}
} else {
for ( i in elems ) {
value = callback( elems[ i ], i, arg );
if ( value != null ) {
ret.push( value );
}
}
}
//把数组扁平化
return concat.apply( [], ret );
}
})
数组扁平化:
let aa = [[1,2,3,4],[2,3,4,5]]
let bb = [].concat(aa)
bb: [1,2,3,4,2,3,4,5]
前面是预热,接下来看这个好吧
jQuery.each( {
parent: function( elem ) {
var parent = elem.parentNode;
return parent && parent.nodeType !== 11 ? parent : null;
},
parents: function( elem ) {
return dir( elem, "parentNode" );
},
parentsUntil: function( elem, i, until ) {
return dir( elem, "parentNode", until );
},
next: function( elem ) {
return sibling( elem, "nextSibling" );
},
prev: function( elem ) {
return sibling( elem, "previousSibling" );
},
nextAll: function( elem ) {
return dir( elem, "nextSibling" );
},
nextUntil: function( elem, i, until ) {
return dir( elem, "nextSibling", until );
},
siblings: function( elem ) {
return siblings( ( elem.parentNode || {} ).firstChild, elem );
},
children: function( elem ) {
return siblings( elem.firstChild );
}
}
// Support: IE 9 - 11 only, iOS 7 only, Android Browser <=4.3 only
// Treat the template element as a regular one in browsers that
// don't support it.
if ( nodeName( elem, "template" ) ) {
elem = elem.content || elem;
}
return 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.uniqueSort( matched );
}
// Reverse order for parents* and prev-derivatives
if ( rparentsprev.test( name ) ) {
matched.reverse();
}
}
return this.pushStack( matched );
};
} );
以上代码是什么意思呢,我来稀释一下。。。
jQuery.fn.parents = function(utill,selector){}
jQuery.fn.parentsUtil = function(utill,selector){}
jQuery.fn.nextUtil = function(utill,selector){}
等等
var matched = jQuery.map( this, fn, until );
这个时候用了map函数,假设此时我们调用$('#qwe').nextUtil('.aa','.bb'),那么情况发展如下:
1. jQuery.map($('#qwe'),nextUtil,'.aa')
2. $('#qwe')是类数组元素,只有一个。会执行nextUtil($('#qwe'),0,'.aa')
3.nextUtil返回的是 dir($('#qwe'),nextSbling,'.aa')
4.我们来看看dir函数
var dir = function( elem, dir, until ) {
var matched = [],
truncate = until !== undefined;
// 这里是是查找#qwe的同辈 .aa节点
while ( ( elem = elem[ dir ] ) && elem.nodeType !== 9 ) {
if ( elem.nodeType === 1 ) {
// 如果碰到了util也就是元素自身,就break
if ( truncate && jQuery( elem ).is( until ) ) {
break;
}
matched.push( elem );
}
}
return matched;
};
注意上面是找出了所有#qwe的同辈.aa元素,但我们想在他碰到.bb就停止,所以有这样一段代码:
if ( selector && typeof selector === "string" ) {
matched = jQuery.filter( selector, matched );
}
我们上面是特殊例子,若是$('.qwe')在页面中有多个,则会走完整个for循环。
我么来看看 jQuery.filter( selector, matched )是如何写的
jQuery.filter = function( expr, elems, not ) {
var elem = elems[ 0 ];
// 过滤不符合expr的
if ( not ) {
expr = ":not(" + expr + ")";
}
if ( elems.length === 1 && elem.nodeType === 1 ) {
return jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [];
}
return jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) {
return elem.nodeType === 1;
} ) );
};
grep: function( elems, callback, invert ) {
var callbackInverse,
matches = [],
i = 0,
length = elems.length,
callbackExpect = !invert;
for ( ; i < length; i++ ) {
callbackInverse = !callback( elems[ i ], i );
if ( callbackInverse !== callbackExpect ) {
matches.push( elems[ i ] );
}
}
return matches;
}
grep是一个过滤函数,invert如果为false,则与callback返回的之相反的元素进去数组。
jQuery.find.matches大家可以到源码去查看一下,这里不具体讲了。
我们来看看jquery里面很重要的一个函数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;
// Return the newly-formed element set
return ret;
}
我们发现在上面任意一个节点查询的函数最后都会有一个,this.pushStack(matched),把查找出来的元素放进去。
---html代码---
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div class="aa"></div>
<div class="bb"></div>
<div class="bb ww"></div>
<div class="bb"></div>
<div class="cc"></div>
<script src="jquery-3.2.1.js"></script>
<script>
console.log($('.aa').nextUntil('.cc'))
</script>
</body>
</html>
2017-07-27 11-54-08屏幕截图.png
相信大家看到了打印结果就很明白了,我们做完节点操作后都会的到一个新的dom对象,我们想回到之前操作的元素可以怎么办呢?
考虑下面代码:
$('.aa').nextUntil('.cc').find('.ww') 此时我们dom对象停在了$('.ww'),如果我们想回到$('.aa')怎么办
end: function() {
return this.prevObject || this.constructor();
},
源码里面有这样一段,返回上次操作的dom元素,使用end方法。
$.fn.grandparent = function() {
var els = this.parent().parent();
return this.pushStack(els.get());
};
$('.child').grandparent().end() 进行了2次查找,但一次end就可以返回最初元素。
关于节点我们再来介绍一个addback方法,结果显然易见。
<html>
<head>
<style>
p, div { margin:5px; padding:5px; }
.border { border: 2px solid red; }
.background { background:yellow; }
.left, .right { width: 45%; float: left;}
.right { margin-left:3%; }
</style>
<script src="http://code.jquery.com/jquery-latest.js"></script>
</head>
<body>
<div class="left">
<p><strong>Before <code>addBack()</code></strong></p>
<div class="before-addback">
<p>First Paragraph</p>
<p>Second Paragraph</p>
</div>
</div>
<div class="right">
<p><strong>After <code>addBack()</code></strong></p>
<div class="after-addback">
<p>First Paragraph</p>
<p>Second Paragraph</p>
</div>
</div>
<script>
$("div.left, div.right").find("div, div > p").addClass("border");
// First Example
$("div.before-addback").find("p").addClass("background");
// Second Example
$("div.after-addback").find("p").addBack().addClass("background");
</script>
</body>
</html>
2. 元素操作
相信下面一段代码大家会经常用到,控制显示和隐藏
jQuery.fn.extend( {
show: function() {
return showHide( this, true );
},
hide: function() {
return showHide( this );
},
toggle: function( state ) {
if ( typeof state === "boolean" ) {
return state ? this.show() : this.hide();
}
return this.each( function() {
if ( isHiddenWithinTree( this ) ) {
jQuery( this ).show();
} else {
jQuery( this ).hide();
}
} );
}
} );
原本简单的功能其实在源码里面也有复杂的判断,来看showHide函数
function showHide( elements, show ) {
var display, elem,
values = [],
index = 0,
length = elements.length;
// Determine new display value for elements that need to change
for ( ; index < length; index++ ) {
elem = elements[ index ];
if ( !elem.style ) {
continue;
}
display = elem.style.display;
if ( show ) {
// Since we force visibility upon cascade-hidden elements, an immediate (and slow)
// check is required in this first loop unless we have a nonempty display value (either
// inline or about-to-be-restored)
if ( display === "none" ) {
values[ index ] = dataPriv.get( elem, "display" ) || null;
if ( !values[ index ] ) {
elem.style.display = "";
}
}
if ( elem.style.display === "" && isHiddenWithinTree( elem ) ) {
values[ index ] = getDefaultDisplay( elem );
}
} else {
if ( display !== "none" ) {
values[ index ] = "none";
// Remember what we're overwriting
dataPriv.set( elem, "display", display );
}
}
}
// Set the display of the elements in a second loop to avoid constant reflow
for ( index = 0; index < length; index++ ) {
if ( values[ index ] != null ) {
elements[ index ].style.display = values[ index ];
}
}
return elements;
}
showHide如果跟的参数是true,就代表是显示,dataPriv.set( elem, "display", display )是用来干嘛的呢,记录元素上一次的display属性
有一个data的原型函数如下:
function Data() {
this.expando = jQuery.expando + Data.uid++;
}
Data.uid = 1;
Data.prototype = {
cache: function( owner ) {
var value = owner[ this.expando ];
if ( !value ) {
value = {};
if ( acceptData( owner ) ) {
if ( owner.nodeType ) {
owner[ this.expando ] = value;
} else {
Object.defineProperty( owner, this.expando, {
value: value,
configurable: true
} );
}
}
}
return value;
},
set: function( owner, data, value ) {
var prop,
cache = this.cache( owner );
// Handle: [ owner, key, value ] args
// Always use camelCase key (gh-2257)
if ( typeof data === "string" ) {
cache[ jQuery.camelCase( data ) ] = value;
// Handle: [ owner, { properties } ] args
} else {
// Copy the properties one-by-one to the cache object
for ( prop in data ) {
cache[ jQuery.camelCase( prop ) ] = data[ prop ];
}
}
return cache;
},
get: function( owner, key ) {
return key === undefined ?
this.cache( owner ) :
owner[ this.expando ] && owner[ this.expando ][ jQuery.camelCase( key ) ];
}
其实就是一段数据储存功能的函数,都有cache而已。咱们接着往下看
jQuery.fn.extend( {
detach: function( selector ) {
return remove( this, selector, true );
},
remove: function( selector ) {
return remove( this, selector );
},
text: function( value ) {
return access( this, function( value ) {
return value === undefined ?
jQuery.text( this ) :
this.empty().each( function() {
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
this.textContent = value;
}
} );
}, null, value, arguments.length );
},
append: function() {
return domManip( this, arguments, function( elem ) {
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
var target = manipulationTarget( this, elem );
target.appendChild( elem );
}
} );
},
prepend: function() {
return domManip( this, arguments, function( elem ) {
if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
var target = manipulationTarget( this, elem );
target.insertBefore( elem, target.firstChild );
}
} );
},
before: function() {
return domManip( this, arguments, function( elem ) {
if ( this.parentNode ) {
this.parentNode.insertBefore( elem, this );
}
} );
},
after: function() {
return domManip( this, arguments, function( elem ) {
if ( this.parentNode ) {
this.parentNode.insertBefore( elem, this.nextSibling );
}
} );
},
empty: function() {
var elem,
i = 0;
for ( ; ( elem = this[ i ] ) != null; i++ ) {
if ( elem.nodeType === 1 ) {
// Prevent memory leaks
jQuery.cleanData( getAll( elem, false ) );
// Remove any remaining nodes
elem.textContent = "";
}
}
return this;
},
我们看到了一堆耳熟能详的jquery方法,我们来看看是如何定义的。
detach: function( selector ) {
return remove( this, selector, true );
},
删除一个元素并将他的返回值保留,以后可能会再加上去。
function remove( elem, selector, keepData ) {
var node,
nodes = selector ? jQuery.filter( selector, elem ) : elem,
i = 0;
for ( ; ( node = nodes[ i ] ) != null; i++ ) {
if ( !keepData && node.nodeType === 1 ) {
jQuery.cleanData( getAll( node ) );
}
if ( node.parentNode ) {
if ( keepData && jQuery.contains( node.ownerDocument, node ) ) {
setGlobalEval( getAll( node, "script" ) );
}
node.parentNode.removeChild( node );
}
}
return elem;
}
detach() 会保留所有绑定的事件、附加的数据,这一点与 remove() 不同。
其实无论remove还是detach都能够保留原始元素,如果是remove会调用cleanData方法,来看一下吧
cleanData: function( elems ) {
var data, elem, type,
special = jQuery.event.special,
i = 0;
for ( ; ( elem = elems[ i ] ) !== undefined; i++ ) {
if ( acceptData( elem ) ) {
if ( ( data = elem[ dataPriv.expando ] ) ) {
if ( data.events ) {
for ( type in data.events ) {
if ( special[ type ] ) {
jQuery.event.remove( elem, type );
// This is a shortcut to avoid jQuery.event.remove's overhead
} else {
jQuery.removeEvent( elem, type, data.handle );
}
}
}
elem[ dataPriv.expando ] = undefined;
}
if ( elem[ dataUser.expando ] ) {
// Support: Chrome <=35 - 45+
// Assign undefined instead of using delete, see Data#remove
elem[ dataUser.expando ] = undefined;
}
}
}
}
我们可以看到cleanData把元素的data和event都给清除了
3. 其他技巧
jQuery.fn.extend( {
hover: function( fnOver, fnOut ) {
return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
}
} );
我们用jquery写一个鼠标上去个鼠标下来事件,上面一个函数很轻松就解决了。
trigger: function( type, data ) {
return this.each( function() {
jQuery.event.trigger( type, data, this );
} );
}
我们在个一个元素绑定事件后,会用trigger来主动触发这个绑定的事件,还可以给他传递一些参数。
下面就会看到我们经常用的动画函数了
jQuery.fn.extend( {
fadeTo: function( speed, to, easing, callback ) {
return this.filter( isHiddenWithinTree ).css( "opacity", 0 ).show()
.end().animate( { opacity: to }, speed, easing, callback );
},
animate: function( prop, speed, easing, callback ) {
var empty = jQuery.isEmptyObject( prop ),
optall = jQuery.speed( speed, easing, callback ),
doAnimation = function() {
// Operate on a copy of prop so per-property easing won't be lost
var anim = Animation( this, jQuery.extend( {}, prop ), optall );
// Empty animations, or finishing resolves immediately
if ( empty || dataPriv.get( this, "finish" ) ) {
anim.stop( true );
}
};
doAnimation.finish = doAnimation;
return empty || optall.queue === false ?
this.each( doAnimation ) :
this.queue( optall.queue, doAnimation );
}
Jquery.speed是一个控制动画速度的函数,来看看如何定义
jQuery.speed = function( speed, easing, fn ) {
var opt = speed && typeof speed === "object" ? jQuery.extend( {}, speed ) : {
complete: fn || !fn && easing ||
jQuery.isFunction( speed ) && speed,
duration: speed,
easing: fn && easing || easing && !jQuery.isFunction( easing ) && easing
};
//注意这里飘逸的js语法,一般speed给的是数字,所以opt实际是一个运动对象。
// Go to the end state if fx are off
if ( jQuery.fx.off ) {
opt.duration = 0;
} else {
if ( typeof opt.duration !== "number" ) {
if ( opt.duration in jQuery.fx.speeds ) {
opt.duration = jQuery.fx.speeds[ opt.duration ];
} else {
opt.duration = jQuery.fx.speeds._default;
}
}
}
if ( opt.queue == null || opt.queue === true ) {
opt.queue = "fx";
}
// Queueing
opt.old = opt.complete;
//重新定义了complete函数,dequeue是干嘛的呢。
opt.complete = function() {
if ( jQuery.isFunction( opt.old ) ) {
opt.old.call( this );
}
if ( opt.queue ) {
jQuery.dequeue( this, opt.queue );
}
};
return opt;
};
由于dequeue涉及队列比较复杂,自己可以去看源码,最后来看一个很重的函数。
jQuery.fn.extend( {
offset: function( options ) {
// 如果options存在,则是设置他的offset
if ( arguments.length ) {
return options === undefined ?
this :
this.each( function( i ) {
jQuery.offset.setOffset( this, options, i );
} );
}
var doc, docElem, rect, win,
elem = this[ 0 ];
if ( !elem ) {
return;
}
if ( !elem.getClientRects().length ) {
return { top: 0, left: 0 };
}
rect = elem.getBoundingClientRect();
doc = elem.ownerDocument;
docElem = doc.documentElement;
win = doc.defaultView;
return {
top: rect.top + win.pageYOffset - docElem.clientTop,
left: rect.left + win.pageXOffset - docElem.clientLeft
};
},
我们想获得某个元素距离窗口左边和上边的距离可以用这个方法,解释一下getBoundingClientRect
rectObject = object.getBoundingClientRect();
DOMRect 对象包含了一组用于描述边框的只读属性——left、top、right和bottom,单位为像素。除了 width 和 height 外的属性都是相对于视口的左上角位置而言的。
好了,今天就先讲到这里了,以后有机会在继续把最后一期给做完吧。。。