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;
    };
目前先把整個源碼流程過一遍,學習其整個流程原理,然後再寫自己的思考其深入學習。


發佈了41 篇原創文章 · 獲贊 15 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章