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