浅析jQuery整体框架与实现(下)

718 查看

前言

分析源码的过程总是成就感与挫败感相伴的,尤其是jquery这样庞大且晦涩难懂的源码,本文承接上一篇:浅析jQuery整体框架与实现(上),继续做更细致些的分析,上篇文章距离现在已经大半年了,本来是只打算写一篇,做个样子的,但看到那么多点赞和收藏的,于是架不住大家的热情,就偷偷把标题改了下,预示着还有下文。上篇文章分析的是jquery-1.7.1,这次分析下最新版的jquery-2.1.4

    (typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
    
    var arr = [];//
    
    var slice = arr.slice;
    
    var concat = arr.concat;
    
    var push = arr.push;
    
    var indexOf = arr.indexOf;
    
    var class2type = {};
    
    var toString = class2type.toString;
    
    var hasOwn = class2type.hasOwnProperty;
    
    var support = {};
})

以上代码主要是让后续代码变得更加简洁,主要是数组和对象的一些原生方法。

class2type初始化后的结构如下:

{ 
"[object Array]": "array" 
"[object Boolean]": "boolean" 
"[object Date]": "date" 
"[object Function]": "function" 
"[object Number]": "number" 
"[object Object]": "object" 
"[object RegExp]": "regexp"
"[object String]": "string"
}

构建一个空数组,该数组具有length,prototypeconstructor属性:

<!--length属性、prototype属性、constructor属性-->
<script type="text/javascript">
    var arr = [];//字面量不会调用Array构造函数
    
    alert(arr.length);//o
    alert(Array.prototype.push(1));//1
    alert(arr.constructor);//function Array() {[native code]}
</script>

73行开始:

var jQuery = function( selector, context ) {    //定义类 
    
        return new jQuery.fn.init( selector, context );//返回选择器的实例  
    },

当我们调用jQuery的时候会返回new init()的结果而不是直接new jQuery()

原型属性和方法

原型属性和方法从92行开始源码如下:

 // 原型属性和方法
jQuery.fn = jQuery.prototype = {

    jquery: version,//版本号

    constructor: jQuery,//指向构造函数jQuery

    selector: "",//从一个空的选择器开始

    length: 0,//指定默认的jQuery对象的长度为0

    toArray: function() {
        return slice.call( this );
    },

    
110 get: function( num ) {
111        return num != null ?
112
113            
114            ( num < 0 ? this[ num + this.length ] : this[ num ] ) :
115
116            
117            slice.call( this );
118    },

122    pushStack: function( elems ) {
123
124        
125        var ret = jQuery.merge( this.constructor(), elems );
126
127
128        ret.prevObject = this;
129        ret.context = this.context;
130
131        return ret;
    },

    
    each: function( callback, args ) {
        return jQuery.each( this, callback, args );
    },

    map: function( callback ) {
        return this.pushStack( jQuery.map(this, function( elem, i ) {
            return callback.call( elem, i, elem );
        }));
    },

    slice: function() {
        return this.pushStack( slice.apply( this, arguments ) );
    },

    first: function() {
        return this.eq( 0 );
    },

    last: function() {
        return this.eq( -1 );
    },

    eq: function( i ) {
        var len = this.length,
            j = +i + ( i < 0 ? len : 0 );
        return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );
    },

    

    
    push: push,
    sort: arr.sort,
    splice: arr.splice
};

.toArray()

.toArray()将当前jQuery对象转换成真正的数组,执行时通过方法call()apply()指定方法执行的环境,即关键字this所引用的对象.

slice.call( this ) == [].slice.call(this);

也就是说该方法在执行时,会将原本属于数组对象的slice方法交给当前执行环境(this所引用的对象)的对象去处理。

看下面一个实例:

 function add() {
//    this.name = "math";
    alert(this.name);
}

//使用new function 定义JavaScript中的用户自定义对象
var sub = new function (){
    this.name = "English";
    alert(this.name);//English
};


add.call(sub);//English,sub调用了add方法,并将sub对象替换为add对象

.get

.get([index]):中括号表示可选。该方法返回当前jQuery对象中指定位置的元素或包含了全部元素的数组。如果指定参数index,则返回一个单独的元素;参数index从0开始,并且支持负数,负数表示从末尾开始算起。

114行首先判断num是否小于0,则用length+num重新计算下标,然后用[]获取指定位置的元素。如果num大于等于0,则直接返回指定位置的元素。否则调用[].slice.call(this)返回所有元素,存入空数组里。

pushStack

pushStack(elems):该原型方法创建一个空的jQuery对象,然后把DOM元素集合放入这个jQuery对象中,并保留对当前jQuery对象的引用。

125行:首先构造一个新的空jQuery对象ret,this.constructor指向构造函数jQuery(),然后把参数elems 合并到this.constructor并赋给新的jQuery对象ret

131行:最后返回新的jQuery对象ret.

关于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;
    },

.end()

.end():该方法结束当前链条中最近的筛选操作,并将匹配元素集合还原为之前的状态。相关代码如下:

166    end: function() {
167                return this.prevObject || this.constructor(null);
168            },
    

167行:返回前一个jQuery对象,如果属性prevObject不存在,则构建一个空的jQuery对象返回。

.pushStack()方法用于入栈,.end()方法用于出栈。

.slice()

.slice():该方法先使用[].slice从当前jQuery对象中获取指定范围的子集,再调用方法.pushStack()把子集转换成jQuery对象

调用关系

javascript的世界中一共有四种上下文调用方式:方法调用模式函数调用模式构造器调用模式apply调用模式

☑  jQuery.extend调用的时候上下文指向的是jQuery构造器

☑  jQuery.fn.extend调用的时候上下文指向的是jQuery构造器的实例对象了

DOM遍历 Traversing

DOM遍历有如下3个核心函数:

| jQuery.dir( elem, dir, until ) | 从一个元素出发,迭代检索某个方向上的所有元素并记录,直到遇到document对象或遇到until匹配的元素 |
| ------------- |:-------------:|
| jQuery.nth( cur, result, dir, elem ) | 从一个元素出发,迭代检索某个方向上的第N个元素|
| jQuery.sibling( n, elem ) |元素n的所有后续兄弟元素,包含n,不包含elem

其中elemdom对象, dir是迭代方向,可选值:parentNode 、nextSibling、 previousSiblinguntil是截至条件

jQuery.dir的整个运行过程是循环查找elemdir的属性, 直到没有后续元素 或者找到了document根节点(elem.nodeType !== 9) , 最后再将所有查找到的元素放到数组中返回。

jQuery.each({
    //父元素
    parent: function( elem ) {
        var parent = elem.parentNode;
        return parent && parent.nodeType !== 11 ? parent : null;
    },
    //祖先元素
    parents: function( elem ) {
        return jQuery.dir( elem, "parentNode" );//检索所有父元素,直至document
    },
    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 )  { ... };
});

jQuery.each( object, callback ) ;//callback(键,值)
$.each(array,callback);//callback(索引,索引值)

jquery.extend()

    jQuery.extend({
        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;
        },
        
        ////返回n的所有兄弟节点,不包括elem
        sibling: function( n, elem ) {
            var matched = [];//定义空数组,用来保存查找到的元素
    
            for ( ; n; n = n.nextSibling ) {//语句1可为空
                if ( n.nodeType === 1 && n !== elem ) {
                    matched.push( n );
                }
            }
    
            return matched;
        }
    });

API的解释

parent API 返回父节点并且当父节点的节点类型不为DocumentFragment(不属于文档树,继承的 parentNode 属性总是 null) 节点的话,就直接返回父节点,否则就返回null

parents API 通过jQuery.dir循环查找elem元素的所有父节点(parentNode),直至document,然后将其返回。

sibling(不同于siblings API)有两个参数, n是起始dom对象, elem是结束dom对象。它通过不断寻找nextSibling, 直到找到非element的对象(n.nodeType === 1) 或者找到了elem为止,,然后将所有查找到的兄弟元素放到数组中返回。值得注意的是,sibling并不是jquery的一个对外提供的API,而是内部使用的,siblings才是对外提供的API

next API 通过sibling方法来获取当前节点的下个元素节点,同理prev API

siblings API 利用sibling方法,先通过父元素的第一个子元素,然后不断往下找下一个紧邻元素,判断剔除自己。

children API 也是利用sibling方法,来引用所有子元素。

DOM操作

jQuery针对DOM操作的插入方法有如下10种:

append、prepend、before、after、replaceWith

appendTo、prependTo、insertBefore、insertAfter、replaceAll

核心函数domManip

.domManip():第一个参数是arguments,第二个参数是回调函数

args:待插入的DOM元素或HTML代码
callback 回调函数,执行格式为callback.call (目标元素即上下文, 待插入文档碎片/单个DOM元素 )

将html转化成dom:

if ( l ) {
            fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this );
            first = fragment.firstChild;

            if ( fragment.childNodes.length === 1 ) {
                fragment = first;
            }
}

append和prepend

相关源码(5204行开始)如下:

    jQuery.fn.extend({
        
        append: function() {
            return this.domManip( arguments, function( elem ) {
                if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                    var target = manipulationTarget( this, elem );
                    target.appendChild( elem );
                }
            });
        },
    
        prepend: function() {
            return this.domManip( arguments, function( elem ) {
                if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) {
                    var target = manipulationTarget( this, elem );
                    target.insertBefore( elem, target.firstChild );
                }
            });
        },
})

jQuery.fn.extend()函数用于为jQuery扩展一个或多个实例属性和方法(主要用于扩展方法)。

append API是被选元素的结尾(仍然在内部)插入指定内容。

text()和html()

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 );
    },

html API 有点长,这里就不列出来了。access()方法用来判断keyvalue值的不同类型。并为.attr(),.prop,.css()提供支持

队列 Queue

我们知道,队列的特点是先进先出,即最先插入的元素最先被删除,有别于栈的先进后出。队列在jQuery源码中,主要用于动画队列中。从6713行处开始到6730行结束的源码如下:

animate: function( prop, speed, easing, callback ) {
        var empty = jQuery.isEmptyObject( prop ),//empty是布尔值
            optall = jQuery.speed( speed, easing, callback ),//修正参数
            doAnimation = function() {

                var anim = Animation( this, jQuery.extend( {}, prop ), optall );

                
                if ( empty || data_priv.get( this, "finish" ) ) {
                    anim.stop( true );
                }
            };
            doAnimation.finish = doAnimation;

        return empty || optall.queue === false ?
            this.each( doAnimation ) :
            this.queue( optall.queue, doAnimation );
    },

其中isEmptyObject()方法的源码如下:

isEmptyObject: function( obj ) {
        var name;
        for ( name in obj ) {
            return false;
        }
        return true;
    },

从源码中可见,通过for…in语句遍历对象属性并返回布尔值。

 console.log(jQuery.isEmptyObject({}));//true

为什么要引入队列?

我们可能已习惯于线性地编写代码,而事实上,对于js编程,比如setTimeout,CSS3 Transition/Animation,ajax,dom的绘制,postmessage,Web Database等等,大量异步操作所带来的回调函数会把我们的代码逻辑弄得支离破碎的。

所以,引入队列可以被认为是允许一系列函数被异步地调用而不会阻塞程序

类型检测

jQuery类型检测的方法主要是如下几个:

jQuery.isFunction( obj )
jQuery.isArray( obj )
jQuery.isWindow( obj )
jQuery.isNumeric( value )
jQuery.type( obj )
jQuery.isPlainObject( object )
jQuery.isEmptyObject( object )

jQuery.type

    type: function( obj ) {
        if ( obj == null ) {
            return obj + "";
        }
         
305            return typeof obj === "object" || typeof obj === "function" ?
306                class2type[ toString.call(obj) ] || "object" :
307                typeof obj;//
308        },

jQuery.type( obj ) 判断参数类型,如果参数是undefinednull,则返回"undefined"或"null";如果参数是js内部对象,则返回对应的字符串名称。

305~307行:首先用typeof检测参数数据类型,如果是Objectfunction类型,则利用Object的原型方法toString()获取参数obj的字符串表示,或直接一律返回“Object”,否则直接typeof 参数的数据类型返回

console.log(typeof {});//"object",带双引号
Object.prototype.toString.call(true);//"[object Boolean]"
{}.toString === Object.prototype.toString;//true

其中,jQuery.isFunction(obj)用于判断传入的参数是否是函数 ,该方法依赖于jQuery.type(obj),该方法通过返回是否是 "function"来实现的

jQuery.isWindow

261~263行源码如下:

isWindow: function( obj ) {
        return obj != null && obj === obj.window;
    },

isWindow(obj) 是用来判断传入参数是不是window对象,首先判断参数是否不为null然后利用等性运算符判断参数对其自身的引用

$.isWindow(window);//true
$.isWindow(document);//false
$.isWindow(iframe);//true 

jQuery.isNumeric

265~271行源码如下:

isNumeric: function( obj ) {
        
        return !jQuery.isArray( obj ) && (obj - parseFloat( obj ) + 1) >= 0;
    },

isNumeric(obj)用来判断参数是否是数字,该函数和isWindow() 一样属于全局jQuery对象。

jQuery.isFunction

255~257行源码如下:

isFunction: function( obj ) {
        return jQuery.type(obj) === "function";
    },

jQuery事件

主要源码结构如下:

jQuery.event = {
    global : {},
    //绑定事件句柄
    add : function(){},
    //移除
    remove : function(){},
    // 触发
    trigger : function(){},
    //分派(执行)事件处理函数
    dispatch : function(){}
    // 执行
    handlers : function(){},
    props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
    fixHooks: {},
    keyHooks: {},
    mouseHooks: {},
    //fix修正event对象
    fix : {},
    special: {
        load: {},
        focus: {},
        blur: {},
        click: {},
        beforeunload :{},
    }
    simulate: {}
}

jQuery事件对象原型

jQuery.Event.prototype = {
    isDefaultPrevented: returnFalse,
    isPropagationStopped: returnFalse,
    isImmediatePropagationStopped: returnFalse,
    
    //阻止默认浏览器默认行为
    preventDefault: function() {
        var e = this.originalEvent;

        this.isDefaultPrevented = returnTrue;

        if ( e && e.preventDefault ) {
            e.preventDefault();
        }
    },
    
    //阻止事件传播
    stopPropagation: function() {
        var e = this.originalEvent;

        this.isPropagationStopped = returnTrue;

        if ( e && e.stopPropagation ) {
            e.stopPropagation();
        }
    },
    //立即停止事件传播
    stopImmediatePropagation: function() {
        var e = this.originalEvent;

        this.isImmediatePropagationStopped = returnTrue;

        if ( e && e.stopImmediatePropagation ) {
            e.stopImmediatePropagation();
        }

        this.stopPropagation();
    }
};

其他事件

jQuery.fn.extend({
    hover: function( fnOver, fnOut ) {
        return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver );
    },

    bind: function( types, data, fn ) {
        return this.on( types, null, data, fn );
    },
    unbind: function( types, fn ) {
        return this.off( types, null, fn );
    },

    delegate: function( selector, types, data, fn ) {
        return this.on( types, selector, data, fn );
    },
    undelegate: function( selector, types, fn ) {
         
        return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn );
    }
});

博主近来穷的吃土了,如果看完本文的内容后觉得对你有所帮助,想博主更新的勤一点,你不扫一下吗(留言你想学的前端技术,博主有空了就更新呦)

<segmentFault/>