关于Jquery源码的解读网上已经有很多文章了,这里可以提供一个比较详细的链接jQuery源码分析系列——来自Aaron,而本帖主要总结一下Jquery源码中一些好的技巧,可以根据自己的需要应用在我们平时编写的代码中。
平常我们新建一个对象的方法是通过new关键字来生成的,如下
var a = function(selector, context) {
//构造函数
}
//通过new关键字生成对象
var newA = new a(sel, con);
而Jquery没有使用new,而是直接 $('')
进行构造,
// 无 new 构造
$('#test').val();
这也是 jQuery 十分便捷的一个地方,当我们使用$('')
的时候,其本质就是相当于 new jQuery(),那么在 jQuery 内部是如何实现的呢?贴这部分源码出来看一下:
重要的二个步骤已经被我用红线框标出来了,我们先看第一处,首先在jquery构造函数内部返回了原型中的init对象,如果我们只做这一步,看下会怎样,例子如下:
var aQuery = function(selector, context) {
return aQuery.prototype.init();
}
aQuery.prototype = {
init: function() {
this.age = 18
return this;
},
name: function() {},
age: 20
}
aQuery().age //结果是18,而不是输出20
很明显,最后一句执行的结果跟预想的不一样,因为new出来的init对象和aQuery不是同一个对象,new出来的init对象age参数为18,并不是20。那么调用aQuery().name()
时也会报错,无法找到这个方法,init对象并没有name这个方法,所以很明显new出来的init跟aQuery类的this分离了。那么怎么让new出来的init对象既能隔离作用域还能使用jquery原型上的参数和方法呢,实现的关键点就是红框第二处
// Give the init function the jQuery prototype for later instantiation
jQuery.fn.init.prototype = jQuery.fn;
通过原型传递解决问题,这里的fn就是jquery的prototype,意思就是把jquery的原型传递给init的原型,这样通过new出来的init也就可以访问jquery原型上的所有参数和方法了,问题完美解决。
你可以把它理解成一种匹配机制,就是我们在代码中设置一些钩子,然后程序执行时自动去匹配这些钩子,我们以一个例子来看下到底什么是钩子机制:
例如我们在向后台进行ajax请求的时候,后台经常会返回我们一些常见的错误码,如:001代表用户不存在,002代表用户密码输入错误,这个时候我们要将错误友好的提示给用户,代码如下:
$.ajax(option,function(result){
var errCode = result.errCode ;//错误码
if(errCode){
if(errCode =='001'){
alert("用户不存在")
}else if(errCode =='002'){
alert("密码输入错误")
}else if(errCode =='003'){
alert("用户被锁定")
}
}else{
//登录成功
}
},function(err){
})
这样写用户提示的话就会用比较多的if..else语句,而且增加状态提示的时候修改也比较麻烦,如果用钩子方法会简单很多,修改后的代码如下:
//状态钩子
var codeList = {
"001":"用户不存在",
"002":"密码输入错误",
"003":"用户被锁定"
}
//下面会调用状态钩子
$.ajax(option,function(result){
var errCode = result.errCode ;//错误码
if(!errCode){
alert(codeList[errCode]); //这里只需要写codeList[errCode]就会自己取对应状态的值
}else{
//登录成功
}
},function(err){
})
这样写的话代码结构就更加清楚了,提高了程序的执行效率,减少了if…else… 的使用,而且如果需要增加状态码的话只需要在codeList 对象里面增加一个就好了,并不需要修改业务逻辑代码。
jquery源码在处理浏览器兼容性、属性操作的时候主要就用了钩子机制,这里我以Jquery中判断变量类型的type方法来具体看一下:
var class2type={};
var toString = Object.prototype.toString;
//给class2type对象赋值
jQuery.each("Boolean Number String Function Array Date RegExp Object Error Symbol",function(index,name){
class2type["Object"+" name"]==name.toLowerCase();
})
//取class2type对应的值进行调用
type:function(obj){
if(obj==null){
return obj+"";
}
return typeof obj =="Object"||typeof obj ==="function"?class2type[toString.call(obj)]|||"object":typeof obj
}
上面代码给class2type赋值后,就变为下面的值:
class2type = {
' [object Boolean]': 'boolean',
' [object Number]': 'number',
' [object String]': 'string',
' [object Function]': 'function',
' [object Undefined]': 'undefined',
' [object Null]': 'null',
' [object Array]': 'array',
' [object Date]': 'date',
' [object RegExp]': 'regexp',
' [object Object]': 'object',
' [object Error]': 'error'
};
这个class2type 就成了类型钩子对象,对象对应索引如果在class2type 中存在相应value,则返回value判断,若不存在则返回object类型。
钩子在jquery的.attr(), .prop(), .val() and .css() 四种操作中都有涉及,如属性操作的propFix,propHooks,attrHooks和valHooks,有兴趣的可以自行查看。
用一句话来形容一下钩子就是:钩子是将需要执行的函数或者其他一系列动作注册到一个统一的入口,程序通过调用这个钩子来执行这些已经注册的函数。
相信很多人都用过jquery的链式调用,如:
$('div').show().hide().css('height','300px').toggle()
其实它实现起来非常简单,只要在每个函数后面return this即可。
jquery还可以通过使用 end() 回溯到上一步选中的 jQuery 对象,如
//通过 end() 方法终止在当前在eq(0)上的操作,返回$('div')对象再选择$('div').eq(1)集合进行hide操作
$('div').eq(0).show().end().eq(1).hide();
让我们看看源码是怎么实现的:
jQuery.fn = jQuery.prototype = {
// 将一个 DOM 元素集合加入到 jQuery 栈
// pushStack()方法通过改变一个 jQuery 对象的 prevObject 属性来跟踪链式调用中前一个方法返回的 DOM 结果集合
pushStack: function(elems) {
// this.constructor就是jQuery的构造函数jQuery.fn.init,所以this.constructor()返回一个jQuery对象
// 由于jQuery.merge 函数返回的对象是第二个函数附加到第一个上面,所以 ret 也是一个 jQuery 对象
var ret = jQuery.merge(this.constructor(), elems);
// 给新的 jQuery 对象ret添加属性 prevObject
// 所以也就是为什么通过 prevObject 能取到上一个合集的引用了
ret.prevObject = this;
ret.context = this.context;
return ret;
},
// 回溯链式调用的上一个对象
end: function() {
// 回溯的关键是返回 prevObject 属性
// 而 prevObject 属性保存了上一步操作的 jQuery 对象集合
return this.prevObject || this.constructor(null);
},
// 取当前 jQuery 对象的第 i 个
eq: function(i) {
// jQuery 对象集合的长度
var len = this.length,
j = +i + (i < 0 ? len : 0);
// 利用 pushStack 返回
return this.pushStack(j >= 0 && j < len ? [this[j]] : []);
},
}
从代码可以看出,end()方法返回 prevObject 属性,这个属性记录了上一步操作的 jQuery 对象合集,而prevObject 属性由 pushStack() 方法生成,该方法将一个 DOM 元素集合加入到 jQuery 内部管理的一个栈中,通过改变 jQuery 对象的 prevObject 属性来跟踪链式调用中前一个方法返回的 DOM 结果集合。当我们在链式调用 end() 方法后,内部就返回当前 jQuery 对象的 prevObject 属性,完成回溯。
函数的重载即是一个方法实现多种功能,经常又能 get 又能 set,虽然阅读起来十分不易,但是从实用性的角度考虑,这也是为什么 jQuery 如此受欢迎的原因,如
// 获取 title 属性的值
$('#id').attr('title');
// 设置 title 属性的值
$('#id').attr('title','jQuery');
那么怎么实现函数重载的呢?当然是通过判断函数参数的数量和类型来分别写对应的方法体,我们举个例子,通过判断参数数量来输出不同的问候语:
function sayHi() {
if (arguments.length==1) {
alert(arguments[0] + "你好,我是第一个sayHi方法!");
} else if (arguments.length == 2) {
alert(arguments[0] + "," + arguments[1] + "你好,我是第二个sayHi方法!");
} else if (arguments.length == 3) {
alert(arguments[0] + "," + arguments[1]+","+arguments[2] + "你好,我是第三个sayHi方法!");
}
}
//以不同参数调用sayHi函数
sayHi("Tom"); //Tom你好,我是第一个sayHi方法!
sayHi("Tom", "lucy"); //Tom,lucy你好,我是第二个sayHi方法!
sayHi("Tom","lucy","jame"); // Tom,lucy,jame你好,我是第三个sayHi方法!
当然jquery源码的attr,html,css等方法的实现肯定会比这个复杂,但是大体的思路就是这样的,我们平时编写js函数也可以适时借鉴这种方法。
这里所说的对参数兼容处理意思是用户可能传入各种奇怪的参数类型,而 jQuery 作者想的真的很周到,考虑了用户的多种使用场景,提供了多种对参数的处理。比如css()方法,我们可以传键值对类型的参数,也可以传对象参数,jquery都能读取并生效,代码如下
// 传入键值对
jQuery("#some-selector")
.css("background", "red")
.css("color", "white")
.css("font-weight", "bold")
.css("padding", 10);
// 传入 JSON 对象
jQuery("#some-selector").css({
"background" : "red",
"color" : "white",
"font-weight" : "bold",
"padding" : 10
});
我们自己编写代码时也可以通过typeof 判断参数的类型来对它进行特殊处理,举个例子
function sayHi(value){
if (typeof value === "string"){
console.log(value + "您好,我是string参数!");
}else if(typeof value === "object"){
console.log(value.name + "您好,我是object参数!");
}
}
sayHi("Tom"); //Tom您好,我是string参数!
sayHi({"name":"TOM"}); //TOM您好,我是object参数!
jquery内部的dom操作方法如append,before,after,replaceWith等都是通过调用jQuery.buildFragment生成文档碎片,把所有的新结点附加在其上,然后把文档碎片的内容一次性添加到document中,这样就不需要每次调用原生方法appendChild或其他节点方法的时候都要重新渲染一遍页面了,大大提升了页面的性能,所以说buildFragment有文档缓存的作用。
我们编写js代码时也要时刻有缓存的思想,比如有一个id为parent的父节点,然后要为其不同子节点绑定事件,我们可以先把id为父节点的dom缓存,再分别通过这个parent节点查找其子节点并绑定上事件,而不要每次都去查找这个parent节点,具体代码如下:
// html代码片段
<div id="parent">
<div class="child1">div>
<div class="child2">div>
div>
// ①有缓存的js代码
var parent = $("#parent"); //这里的parent会在浏览器缓存
parent.find(".child1").click(function(){})
parent.find(".child2").click(function(){})
//②没有缓存的js代码
$("#parent .child1").click(function(){})
$("#parent .child2").click(function(){})
代码②片段第一次给子节点child1绑定事件的时候需要去查找一次parent节点,然后通过parent找到child1节点绑定click事件,后面再给child2绑定事件的时候还需要查找一次parent节点,通过parent节点再找到child2节点绑定上click事件;而代码①片段开始就把parent节点赋值给一个参数,这个parent就会再浏览器中缓存,后面再通过parent节点查找它的子元素时就会直接使用缓存的parent节点,而不需要再次去查找这个parent元素,效率明显比代码②片段高。
extend 方法在 jQuery 中是一个很重要的方法,jQuey 内部用它来扩展静态方法或实例方法,而且我们开发 jQuery 插件的时候也会用到它。
其中jQuery.extend(object) 为扩展 jQuery 类本身,为类添加新的静态方法;jQuery.fn.extend(object) 给 jQuery 对象添加实例方法,通过这个 extend 添加的新方法,实例化的 jQuery 对象都能使用,也就是说,使用 jQuery.extend() 拓展的静态方法,我们可以直接使用 $.xxx
进行调用(xxx是拓展的方法名),而使用jQuery.fn.extend() 拓展的实例方法,需要使用$().xxx
调用。
在实现上它们用的是同一个方法体,因为源码是这样的jQuery.extend = jQuery.fn.extend = function() {}
,但为什么实现了不同的功能,这就要归功于javascript强大的this了,在 jQuery.extend() 中,this 的指向是 jQuery 对象;在 jQuery.fn.extend() 中,this 的指向是 fn 对象,也就是jQuery.prototype对象。
这里我想说的是当我们自己封装一个组件或库时也应该注意扩展性,必要的话也可以提供一个接口供用户自定义方法来达到他的需求。
本帖主要介绍了jquery源码中的七个小技巧:无new构造、钩子机制、链式调用及回溯、函数重载、函数参数兼容处理、缓存思想和extend方法,我觉得我们平时编写代码的时候都是可以使用的,可以让我们的代码变得效率更高,结构更清晰,鼓励大家多多应用!