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 外的属性都是相对于视口的左上角位置而言的。


好了,今天就先讲到这里了,以后有机会在继续把最后一期给做完吧。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容