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