jQuery源码学习(3)-构造jQuery对象

1、源码结构

先看总体结构,再做分解:

(function( window, undefined ) {

      // 构建jQuery对象

    //在jQuery原型中定义init这个工厂方法,用于jQuery对象的实例化,是为了避免用jQuery自身实例化的时候造成死循环。

    //init放入原型中,是因为实例this只与原型有关系       

     // jQuery框架分隔作用域的处理

       var  jQuery = function( selector, context ) {

           return new jQuery.fn.init( selector, context, rootjQuery );

       },

       // jQuery对象原型

       jQuery.fn = jQuery.prototype = {

           constructor: jQuery,

           init: function( selector, context, rootjQuery ) {

              // selector有以下6种分支情况(1.6.0版本比2.0.3版多了“body”部分):

              // 字符串:HTML标签、HTML字符串、#id、选择器表达式

              // DOM元素              

              // 函数(作为ready回调函数)

              // 最后返回伪数组

           }

            //实例方法

       };

       // Give the init function the jQuery prototype for later instantiation

        //通过原型传递,使返回的实例能访问jQuery的原型对象

       jQuery.fn.init.prototype = jQuery.fn;

       // 合并内容到第一个参数中,后续大部分功能都通过该函数扩展

       // 通过jQuery.fn.extend扩展的函数,大部分都会调用通过jQuery.extend扩展的同名函数

       jQuery.extend = jQuery.fn.extend = function() {};

      // 在jQuery上扩展静态方法(工具函数)

       jQuery.extend({

           // ready 

           // isPlainObject isEmptyObject

           // parseJSON parseXML

           // globalEval

           // each makeArray inArray merge grep map

           // proxy

           // access

           // uaMatch

           // sub

           // browser

       });

        jQuery.ready.promise=function(obj){

            //在jQuery.ready.promise函数中设置了延时,当延时对象解决的时候执行ready()函数中的fn函数。

        };

 // All jQuery objects should point back to these

    rootjQuery = jQuery(document);

// 到这里,jQuery对象构造完成,后边的代码都是对jQuery或jQuery对象的扩展

   window.jQuery = window.$ = jQuery;

})(window);

通过上诉源码结构,应注意到以下几点:

  • jQuery对象不是通过 new jQuery 创建的,而是通过 new jQuery.fn.init 创建的
var jQuery = function( selector, context ) {
       return new jQuery.fn.init( selector, context, rootjQuery );
}
  • jQuery对象就是jQuery.fn.init对象,如果执行new jQeury(),生成的jQuery对象会被抛弃,最后返回 jQuery.fn.init对象;因此可以直接调用jQuery( selector, context ),没有必要使用new关键字
  • 先执行 jQuery.fn = jQuery.prototype,再执行 jQuery.fn.init.prototype = jQuery.fn,合并后的代码如下:jQuery.fn.init.prototype = jQuery.fn = jQuery.prototype
  • 所有挂载到jQuery.fn的方法,相当于挂载到了jQuery.prototype,即挂载到了jQuery 函数上(一开始的 jQuery = function( selector, context ) ),但是最后都相当于挂载到了jQuery.fn.init.prototype,即相当于挂载到了一开始的jQuery 函数返回的对象上,即挂载到了我们最终使用的jQuery对象上。

2、jQuery链式调用

DOM链式调用的处理:

  • 节约JS代码.
  • 所返回的都是同一个对象,可以提高代码的效率

通过简单扩展原型方法并通过return this的形式来实现跨浏览器的链式调用。

利用JS下的简单工厂模式,来将所有对于同一个DOM对象的操作指定同一个实例。

jQuery().init().name()
分解
a = jQuery();
a.init()
a.name()

把代码分解一下,很明显实现链式的基本条件就是实例this的存在,并且是同一个

jQuery.prototype = {
    init: function() {
        return this;
    },
    name: function() {
        return this
    }
}

所以我们在需要链式的方法访问this就可以了,因为返回当前实例的this,从而又可以访问自己的原型了

优点:节省代码量,提高代码的效率,代码看起来更优雅

缺点:所有对象的方法返回的都是对象本身,也就是说没有返回值,这不一定在任何环境下都适合。

Javascript是无阻塞语言,所以他不是没阻塞,而是不能阻塞,所以他需要通过事件来驱动,异步来完成一些本需要阻塞进程的操作,这样处理只是同步链式,异步链式jquery从1.5开始就引入了 Promise, jQuery.Deferred。

3、扩展插件接口

jQuery的主体框架就是这样,但是根据一般设计者的习惯,如果要为jQuery或者jQuery prototype添加属性方法,同样如果要提供给开发者对方法的扩展,从封装的角度讲是不是应该提供一个接口才对,字面就能看懂是对函数扩展,而不是看上去直接修改prototype.友好的用户接口,

jQuery支持自己扩展属性,对外提供了一个接口,jQuery.fn.extend()来对对象增加方法。

从jQuery的源码中可以看到,jQuery.extend和jQuery.fn.extend其实是同指向同一方法的不同引用

jQuery.extend = jQuery.fn.extend = function() {
    jQuery.extend 对jQuery本身的属性和方法进行了扩展
    jQuery.fn.extend 对jQuery.fn的属性和方法进行了扩展,也就是对jQuery.prototype的拓展,最终表现为对jQuery实例$(...)的拓展。
}

通过extend()函数可以方便快速的扩展功能,不会破坏jQuery的原型结构

jQuery.extend = jQuery.fn.extend = function(){...}; 这个是连等,也就是2个指向同一个函数,怎么会实现不同的功能呢?这就是this 力量了!

extend方法是jQuery中的继承方法,当extend只有一个参数时,代表将对象扩展到jQuery的静态方法或实例方法中,例如:

$.extend({
        a: function () {
            alert("a");
        }
        
})
$.fn.extend({
       a: function () {
           alert("a");
       }
})
$.a(); //jQuery对象调用方法a();
$().a();  //jQuery实例调用方法a();

在上面的代码可以看出不管是jQuery对象还是实例,都可以用extend方法进行继承,在源码中也是调用的同一个方法,之所以可以这么做的原因是因为在源码中,内部绑定时,用到了this。

$.extend的this就是$ 而 $.fn.extend的this是$.fn,也就是代表实例的原型上扩展。

再看一下传入多个参数的情况,当传入多个参数时,如果第一个参数不是bool类型,默认后面的参数的属性都会被添加到一个参数对象上。

如果第一个参数为bool类型且为true,则代表深拷贝,默认为浅拷贝,false。

var a = {};
var b = { tom: { age: 14 } }
$.extend(a, b);
a.tom.age = 25;
console.log(a.tom.age); //25
console.log(b.tom.age);//25  

上面的代码的问题可以看到,当继承的对象属性中有引用类型的时候,那么会造成两个两个对象同时指向一个对象,这样如果改变一个的话,另一个也随之改变,所以:

$.extend(true,a, b);   //把第一个值定为true,进行深拷贝就可以了

针对fn与jQuery其实是2个不同的对象,在之前有讲述:

  • jQuery.extend 调用的时候,this是指向jQuery对象的(jQuery是函数,也是对象!),所以这里扩展在jQuery上。
  • 而jQuery.fn.extend 调用的时候,this指向fn对象,jQuery.fn 和jQuery.prototype指向同一对象,扩展fn就是扩展jQuery.prototype原型对象。
  • 这里增加的是原型方法,也就是对象方法了。所以jQuery的api中提供了以上2中扩展函数。

4、详细源码分析

a、初始化jQuery方法,可以让我们直接jQuery来创建init()的实例,即jQuery对象的创建: 

var jQuery = function(selector, context) {
      // The jQuery object is actually just the init constructor 'enhanced'
      return new jQuery.fn.init(selector, context, rootjQuery);
},

b、jQuery.fn = jQuery.prototype = {};中定义的函数有:

constructor:JQuery 重新指向JQ构造函数

init(): 初始化和参数管理的方法。

selector:存储选择字符串

length:this对象的长度

toArray():转换数组的方法

get():转原生集合

pushStack():jQuery的入栈

each():遍历集合

ready():dom加载的接口。

slice():集合的截取

first():集合的第一项

last():集合的最后一项

eq():返回集合的某项

map():对集合进行遍历操作

end():查找当前对象在栈中的下一个对象

push:数组的push方法 (内部使用)

sort:数组的sort方法(内部使用)

splice:数组的splice方法(内部使用)

jQuery框架的基础就是查询了,查询文档元素对象,jQuery是总入口,选择器支持9种方式的处理:

1.$(document)   
2.$(‘
’) 3.$(‘div’) 4.$(‘#test’) 5.$(function(){}) 6.$("input:radio", document.forms[0]); 7.$(‘input’, $(‘div’)) 8.$() 9.$("
", { "class": "test", text: "Click me!", click: function(){ $(this).toggleClass("test"); } }).appendTo("body"); 10$($(‘.test’))

c、jQuery.fn = jQuery.prototype = {};的源码分析:

  
jQuery.fn = jQuery.prototype = {
        // The current version of jQuery being used
        jquery: core_version,  //对jQuery版本的赋值

        constructor: jQuery,   //重指向,防止给对象原型进行覆盖操作,导致对象原型上的constructor丢失
	//创建对象的工程函数,位于jQuery的原型中
	/*init函数的结构:
	    处理"",null,undefined,false,返回this ,增加程序的健壮性
	    处理字符串
	    处理DOMElement,返回修改过后的this
	    处理$(function(){})*/
        init: function(selector, context, rootjQuery) {
		//selector:$()括号中的第一个参数。
		//如:"#id" ".class" "
  • " document function()等 //context:执行的上下文 //rootJquery:JQ的根对象。 //然后定义变量, var match, elem; // HANDLE: $(""), $(null), $(undefined), $(false)     //检查selector是否为空也就是对 $(""),$(null),$(undefind),$(false) 进判断。 if (!selector) { return this; }     //通过校验之后,接着是判断selector的类型:     //依次对字符串、节点、函数进行判断,并分别进行了单独的处理     /* if ( typeof selector === "string" ) { //实现代码 } else if ( selector.nodeType ) { //实现代码 } else if ( jQuery.isFunction( selector ) ) { //实现代码 }     */ // Handle HTML strings     //匹配模式一:$("#id");     //1、进入字符串处理 if (typeof selector === "string") { //如果selector是html标签组成(且不是空标签),直接match = [null, selector, null]; if (selector.charAt(0) === "<" && selector.charAt(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 {     //否则的话,利用前文定义的rquickExpr正则表达式进行匹配     //例如:$("#id"),$(".class"),$("div") 这种形式的。 match = rquickExpr.exec(selector); } //匹配模式二: // math不为null,并且macth[1]存在 //那么这就代表创建标签的语句满足条件,或者context为空, //context为空代表是选择id,因为id没有上下文, //所以满足这个条件的有:$("
  • "),$("#id") if (match && (match[1] || !context)) {     //处理(html)->(array),也就是处理的是HTML方式 // HANDLE: $(html) -> $(array)     //判断是创建标签还是id if (match[1]) { //创建标签 context = context instanceof jQuery ? context[0] : context; //目的:将context赋值为原生的节点 /*在创建标签时,有是可能需要第二参数,这个第二个参数也就是执行上下文, 例如:$("
  • ",document) 一般很少这样使用, 但是当页面中有iframe时,想在iframe中创建, 那么第二参数设置为iframe后,就在iframe中创建了。*/ //jQuery.parseHTML功能:使用原生的DOM元素的创建函数将字符串转换为一组DOM元素, //然后,可以插入到文档中。parseHTML函数代码见代码extend函数中 var aaa = jQuery.parseHTML( match[1], context && context.nodeType ? context.ownerDocument || context : document, //传入上下文 /*ownerDocument和 documentElement的区别:     ownerDocument是Node对象的一个属性,返回的是某个元素的根节点文档对象:即document对象     documentElement是Document对象的属性,返回的是文档根节点     对于HTML文档来说,documentElement是标签对应的Element对象,ownerDocument是document对象 */ true ) // scripts is true for back-compat jQuery.merge(this, aaa); //jQuery.merge:合并两个函数的内容到第一个数组 // HANDLE: $(html, props) //这种匹配的是:$("
  • ",{title:"hello",html:"aaaaaaa"}) 后面有个json对象当参数的方式。 /*如果是这种方式的话,那么会循环这个json对象,先判断json里的属性是否是jq自带的方法, 如果是,则直接调用方法,否则,进去else,用jq的attr方法为这个标签加一个属性。*/ if (rsingleTag.test(match[1]) && jQuery.isPlainObject(context)) { for (match in context) { // Properties of context are called as methods if possible if (jQuery.isFunction(this[match])) { this[match](context[match]); // ...and otherwise set as attributes } else { this.attr(match, context[match]); } } } return this; // HANDLE: $(#id) //若为id,则执行下面 } else { elem = document.getElementById(match[2]); // Check parentNode to catch when Blackberry 4.6 returns // nodes that are no longer in the document #6963 /*判断,这是为了最黑莓浏览器的兼容, 因为在黑莓4.6版本的浏览器中,当删除节点之后,还可以用js代码查找到这个节点, 所以需要进行一下父节点的判断,因为任何节点都会有父节点。*/ if (elem && elem.parentNode) { // Inject the element directly into the jQuery object this.length = 1; this[0] = elem; } this.context = document; this.selector = selector; return this; /*返回一个JQ需要的特殊json格式。赋值长度为1, 第一个对象elem是当前查找到的对象。然后把上下文赋值document,赋值selector。*/ } // HANDLE: $(expr, $(...));     /*这段代码的判断就是要保证     $("ul",document).find("li") $("ul",$(document)).find("li")     这两种形式,都会执行:jQuery(document).find();这个方法。*/ } else if (!context || context.jquery) { //context是代码在调用init函数时指定的上下文对象, //也就是jQuery(selector, context)中的context。 return (context || rootjQuery).find(selector); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else {                     //选择器的context为真实的上下文环境,比如$("p",".test"):                     //查找class .test下的p标签元素,等价于$(context).find(expr) return this.constructor(context).find(selector); } // HANDLE: $(DOMElement) /*首先先判断传入的是不是节点,如果是节点,肯定就会有nodeType, 然后设置上下文、长度并返回一个类似数组的json对象。*/ } else if (selector.nodeType) { this.context = this[0] = selector; this.length = 1; return this; // HANDLE: $(function) // Shortcut for document ready //使$(function(){ //代码 }) //和$(documnet).ready(function(){ //代码 })等价。简写 } else if (jQuery.isFunction(selector)) { return rootjQuery.ready(selector); }         /*有时在写代码时可能会这么写:$( $("div") ), 虽然很少有人这么写,但这里也对这种情况进行了处理, 从源码可以看出,这种写法其实最后被转换成:$("div")这种形式。*/ if (selector.selector !== undefined) { this.selector = selector.selector; this.context = selector.context; } //init方法的最后一行,进行返回 //jQuery.makeArry方法是将选择到节点返回一个原生数组 //当传入第二个参数时,会返回一个jQuery需要的json对象 return jQuery.makeArray(selector, this); }, // Start with an empty selector selector: "", // The default length of a jQuery object is 0 length: 0, /*在这里下面定义的都是实例方法,在jQuery内部有实例方法还有工具方法, 工具方式是最底层的方法,有时实例方法会调用工具方法。*/ //这里用到了原生数组的slice方法,这个方法是截取数组的某个一部分, //如果不传值,就返回一个副本,所以这个方法就返回了一个原生数组。 toArray: function() { return core_slice.call(this); }, // Get the Nth element in the matched element set OR // Get the whole matched element set as a clean array //get方法也是返回原生的对象, //如果传值则返回某一个,不传的话则返回一个集合。 get: function(num) { return num == null ? // Return a 'clean' array this.toArray() : //未传值,调用toArray()方法,返回一个数组集合 // Return just the object         //当传入num的时候,先判断是否大于0,如果大于0则直接返回集合中对应的对象, //如果小于0,则倒序查找,如-1,则返回最后一个。 (num < 0 ? this[this.length + num] : this[num]); }, // Take an array of elements and push it onto the stack // (returning the new matched element set) //入栈,先进后出 /*先声明一个ret,然后用merge方法, 将传入的对象和一个空对象合并,也就是this.constructor(), 然后到了最关键的一步,ret.prevObject赋值为this, 也就是说通过这个属性进行关联,以后在查找的时候, 通过prevObject就可以找到了上一个对象了。然后赋值上下文并返回。*/ 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; }, // Execute a callback for every element in the matched set. // (You can seed the arguments with an array of args, but this is // only used internally.) //each方法是又调用了jQuery的工具方法each进行了第二次调用 each: function(callback, args) { return jQuery.each(this, callback, args); }, //ready方法是又调用了jQuery的工具方法jQuery.ready.promise()进行了第二次调用 ready: function(fn) { // Add the callback jQuery.ready.promise().done(fn); return this; }, //jQuery的slice方法和数组中的slice方法基本一致, //只是这里调用了入栈的方法 slice: function() { return this.pushStack(core_slice.apply(this, arguments)); }, //first方法和last方法其实都是在内部调用了eq方法 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);//j才是真正的索引 //当传入的i为负数时,例如-1,则查找最后一个元素。 return this.pushStack(j >= 0 && j < len ? [this[j]] : []); }, //map函数使用例子: /* var arr=[1,2,3]; arr = $.map(arr, function (elem, index) { return elem * index; }) console.log(arr);//[0,2,6] */ map: function(callback) { return this.pushStack(jQuery.map(this, function(elem, i) { return callback.call(elem, i, elem); })); }, //通过prevObject的属性来找到它的下层对象,与pushStack()结合使用 //这里的this.constructor(null)则是为了防止多次调用end, //如果已经调用到尽头,则返回一个空对象。 end: function() { return this.prevObject || this.constructor(null); }, // For internal use only. // Behaves like an Array's method, not like a jQuery method. //把数组的这些方法挂载到这几个变量上,以供内部使用, //另外注释上的意思也说了不建议在外部使用。 push: core_push, sort: [].sort, splice: [].splice };
  • d、接口扩展函数jQuery.extend = jQuery.fn.extend = function() {};

    内部结构:

    jQuery.extend = jQuery.fn.extend = function() {
        //定义一些参数
        if(){}    //看是不是深拷贝的情况。
        if(){}    //看参数是否正确
        if(){}    //看是不是插件的情况
        for(){     //处理多个对象参数
            if(){}             //防止循环调用
            if(){}            //深拷贝
            else if(){}     //浅拷贝
        }
    }        

    源码详解:

    //增加对象的方法,也是两个对外可用户自定义拓展功能的接口
        jQuery.extend = jQuery.fn.extend = function() {
            var options, name, src, copy, copyIsArray, clone,
                target = arguments[0] || {}, //常见用法:jQuery.extend(obj1,obj2),此时,target为qrguments[0]
                i = 1,
                length = arguments.length,
                deep = false;
    
            // Handle a deep copy situation
    		//如果第一个参数是Boolean型,可能是深度拷贝
            if (typeof target === "boolean") {     //如果第一个参数为true,即jQuery.extend(true,obj1,obj2);的情况
                deep = target;                     //此时target是true
                target = arguments[1] || {};       //target改为obj1
                // skip the boolean and the target,跳过Boolean和target,从第3个开始
                i = 2;
            }
    
            // Handle case when target is a string or something (possible in deep copy)
    		//target不是对象也不是函数,则强制设置为空对象
            if (typeof target !== "object" && !jQuery.isFunction(target)) {  //处理奇怪情况,比如:jQuery.extend('hello',{nick:'casper'});
                target = {};
            }
    
            // extend jQuery itself if only one argument is passed
    		//如果只传入一个参数,则认为是对jQuery的扩展
            if (length === i) {       //处理这种情况,jQuery.extend(obj),或jQuery.fn.extend(obj)
                target = this;        //jQuery.extend时,this指的是jQuery;    jQuery.fn.extend时,this指的是jQuery.fn。
                --i;
            }
    
            for (; i < length; i++) {
                // Only deal with non-null/undefined values
    			//只处理非空参数
                if ((options = arguments[i]) != null) {    //比如jQuery.extend(obj1,obj2,obj3,obj4),options则为obj2、obj3...
                    // Extend the base object
                    for (name in options) {
                        src = target[name];
                        copy = options[name];
    
                        // Prevent never-ending loop
                        if (target === copy) {  //防止自引用(循环引用)
                            continue;
                        }
    
                        // Recurse if we're merging plain objects or arrays
    					//如果是深拷贝,且被拷贝的属性值本身是个对象或数组,则递归
                        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
                            if (copyIsArray) {  //被拷贝的属性值copy是个数组
                                copyIsArray = false;
    							//clone为src的修正值
                                clone = src && jQuery.isArray(src) ? src : [];
    
                            } else {             //被拷贝的属性值copy是个plainObject(对象),比如{nick:'casper'}
    							//clone为src的修正值
                                clone = src && jQuery.isPlainObject(src) ? src : {};
                            }
    
                            // Never move original objects, clone them
                            target[name] = jQuery.extend(deep, clone, copy);   //递归调用jQuery.extend
    
                            // Don't bring in undefined values
                        } else if (copy !== undefined) {         //浅拷贝,且属性值不为undefined,不能拷贝空值
                            target[name] = copy;
                        }
                    }
                }
            }
    
            // Return the modified object,返回更改后的对象
            return target;
        };
    目前先把整个源码流程过一遍,学习其整个流程原理,然后再写自己的思考其深入学习。


    你可能感兴趣的:(jQuery源码分析,jQuery对象)