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.$(‘<div>’) 
3.$(‘div’) 
4.$(‘#test’) 
5.$(function(){}) 
6.$("input:radio", document.forms[0]); 
7.$(‘input’, $(‘div’)) 
8.$() 
9.$("<div>", { 
         "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" "<li>" 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);
                }
		//匹配模式二:<htmltag>
                // math不为null,并且macth[1]存在
		//那么这就代表创建标签的语句满足条件,或者context为空,
		//context为空代表是选择id,因为id没有上下文,
		//所以满足这个条件的有:$("<li>"),$("#id")
                if (match && (match[1] || !context)) {
					
		    //处理(html)->(array),也就是处理的是HTML方式
                    // HANDLE: $(html) -> $(array)
		    //判断是创建标签还是id
                    if (match[1]) {   //创建标签
                        context = context instanceof jQuery ? context[0] : context;
			//目的:将context赋值为原生的节点
			/*在创建标签时,有是可能需要第二参数,这个第二个参数也就是执行上下文,
			例如:$("<li>",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是<html>标签对应的Element对象,ownerDocument是document对象
				*/
                            true
                        )

                        // scripts is true for back-compat
                        jQuery.merge(this, aaa);  //jQuery.merge:合并两个函数的内容到第一个数组

                        // HANDLE: $(html, props)
			//这种匹配的是:$("<li>",{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;
    };
目前先把整个源码流程过一遍,学习其整个流程原理,然后再写自己的思考其深入学习。


猜你喜欢

转载自blog.csdn.net/qq_20901397/article/details/80300967