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;
};
目前先把整个源码流程过一遍,学习其整个流程原理,然后再写自己的思考其深入学习。