参考资料:饥人谷任务班任务42视频,jQuery 2.0.3 源码分析core - 整体架构
$
调用方法的实现
jQuery 的每一个实例并没有通过 new
来新建,而是使用 $
和 jQuery
来新增实例并直接进行方法的调用,如:
$(".btn").on("click", function() {
console.log("do sth.");
});
jQuery.each(function() {
console.log("do sth.");
});
为使每次调用 $
或 jQuery
能够创建实例,在构建 jQuery 对象时,返回值应该是对象的实例。
下面尝试以 rQuery 进行重现。
var rQuery = function(selector, context) {
//返回实例
}
rQuery.prototype = {};
如果直接使用 new
创建实例,会产生死循环:
var rQuery = function(selector, context) {
return new rQuery; //没有尽头的递归
}
rQuery.prototype = {};
因此,要想返回正确的实例,必须考虑其他的途径。由于 new
需要针对方法来执行,因此通过返回 rQuery.prototype
来创建实例也是不可行的。但我们可以在原型中新增一个方法,尝试通过返回这个方法来创建实例:
var rQuery = function(selector, context) {
return new rQuery.prototype.init(); //返回实例
}
rQuery.prototype = {
init: function() {
return this; // this 指向实例,也就是 rQuery
}
};
不过,这种方法会带来一个新的问题(如图):所返回的实例和 rQuery 对象并没有对应的关系(淡粉色线),新的实例不能调用 rQuery 中的方法。
var rQuery = function(selector, context) {
return new rQuery.prototype.init(); //返回实例
}
rQuery.prototype = {
init: function() {
return this;
}
print: function() {
console.log(this);
}
};
rQuery.print(); // 报错
因此,必须显性地将 init
方法的原型定义为 rQuery 的原型(红色线):
var rQuery = function(selector, context) {
return new rQuery.prototype.init(); //返回实例
}
rQuery.prototype = {
init: function() {
return this;
}
};
rQuery.prototype.init.prototype = rQuery.prototype;
这样,每次使用 rQuery
时就会产生新的实例,并且可以正常使用 rQuery 原型中定义的方法。
定义 window.$ = window.rQuery = rQuery
即可使用 $
代替 rQuery
进行操作。
链式调用的实现
在 jQuery 中,可以对方法进行链式调用。使用链式调用节约代码、提高效率,简单优雅,可谓 jQuery 的一大魅力。
$(this).removeClass("active").addClass("bye");
要能够进行链式调用,则必须在第一个方法的调用之后返回对象本身:
var $tmp = $(this).removeClass("active"); //$tmp === $(this)
$tmp.addClass("bye"); //相当于 $(this).addClass("bye")
因此,在定义可以使用链式调用的方法时,需要添加 return this;
以保证链式调用能够实现:
var rQuery = function(selector, context) {
return new rQuery.prototype.init(); //返回实例
}
rQuery.prototype = {
init: function() {
return this;
},
setName: function(name) {
this.name = name;
return this;
},
printName: function(name) {
console.log(this.name);
return this;
}
};
rQuery.prototype.init.prototype = rQuery.prototype;
rQuery().setName("Hanna").printName(); //Hanna
插件接口的定义与实现
同其他库一样,jQuery 提供可以让开发者添加扩展的接口。开发者可以通过 jQuery.fn.extend()
来向 jQuery 中添加新的方法。
jQuery 中,同时存在 jQuery.extend()
和 jQuery.fn.extend()
两种方法,从源码中可以发现,二者实际上是指向同一个方法的不同引用:
jQuery.extend = jQuery.fn.extend = function() {
//具体实现
}
这样做的用意是:通过 jQuery.extend()
实现对 jQuery 本身的属性和方法的扩展;而通过 jQuery.fn.extend()
来实现对 jQuery.fn,即 jQuery 的原型的属性和方法进行扩展。同时,两种扩展都不会破坏 jQuery 原有的结构。
之所以在使用 jQuery.extend = jQuery.fn.extend
的前提下仍能够区别出 jQuery 和 jQuery.fn,是因为对 this
的运用。 this
指向的是调用方法的对象。在 jQuery.extend()
中,调用 extend()
方法的是 jQuery
,所以 this
指向 jQuery
,而在 jQuery.fn.extend()
中,调用方法的则是 jQuery.fn
,因此这里 this
指向了 jQuery.fn
,从而成功地区分出了两种不同的扩展方式。
之前曾参考《使用jquery实现简单的拖动效果》实现了一个拖拽效果,拿到这里来试一下 extend()
方法的使用(在线DEMO):
//html
<div class="panel">This div is draggable</div>
//javascript
//drag()
var drag = function() {
$(this).each(function() {
var me = $(this);
var isMoved = false;
var rX, rY;
me.on("mousedown", function(event) {
isMoved = true;
rX = event.pageX - parseInt(me.css("left"));
rY = event.pageY - parseInt(me.css("top"));
me.css("cursor", "move");
return false; //解决拖拽导致的文字选中问题
});
$(document).on("mousemove", function(event) {
if (isMoved) {
var eX = event.pageX;
var eY = event.pageY;
var wX = $(window).width();
var wY = $(window).height();
var thisX = me.width();
var thisY = me.height();
var thisMarginLeft = parseInt(me.css("margin-left"));
var nowX = eX - rX;
var nowY = eY - rY;
var left, top;
var thisLeft = (thisMarginLeft > 0) ? 0 : (-thisMarginLeft);
if (nowX < thisLeft) {
left = thisLeft;
} else if (nowX > wX - thisX + thisLeft) {
left = wX - thisX + thisLeft;
} else {
left = nowX;
}
if (nowY < 0) {
top = 0;
} else if (nowY > wY - thisY) {
top = wY - thisY;
} else {
top = nowY;
}
me.css({
"left": left,
"top": top
});
}
}).on("mouseup", function() {
isMoved = false;
me.css("cursor", "auto");
});
});
};
// 添加到 jQuery 中
jQuery.fn.extend({drag: drag});
// 调用
$(".panel").drag();
可以看出,drag()
方法已成功添加到 jQuery 当中,可以用 $(".panel").drag()
进行调用,并实现拖拽功能。
以 drag()
为例,来看 extend()
方法的实现:
jQuery.extend = jQuery.fn.extend = function() {
var src, copyIsArray, copy, name, options, clone,
target = arguments[0] || {}, // 传入的参数,以第一个参数为目标对象(这里是 {drag: drag})
i = 1,
length = arguments.length,
deep = false; // 深拷贝的参数
// Handle a deep copy situation
if ( typeof target === "boolean" ) { // 如果第一个参数是 true/false,通常为 true
deep = target; //deep 为 true/false
target = arguments[1] || {}; // 将第二个参数作为目标对象
// skip the boolean and the target
i = 2; // 跳过前两个参数,从第三个开始
}
// Handle case when target is a string or something (possible in deep copy)
if ( typeof target !== "object" && !jQuery.isFunction(target) ) { // 如果作为目标对象的参数不是对象类型
target = {};
}
// extend jQuery itself if only one argument is passed
if ( length === i ) { // 如果只有一个参数
target = this; // 就将 jQuery(.fn) 自身作为目标对象(这里,因为只传入了 {drag: drag},因此目标对象为 jQuery(.fn),也就实现了向 jQuery 中添加对象(方法))
--i; // i=0
}
for ( ; i < length; i++ ) {
// Only deal with non-null/undefined values
if ( (options = arguments[ i ]) != null ) { // 这里 options = {drag: drag}
// Extend the base object
for ( name in options ) {
src = target[ name ]; // 向目标对象(这里是 jQuery(.fn)) 中添加属性,并将属性值赋予变量 src,如果目标对象中没有该属性,那么此时 src = undefined(这里 src = undefined)
copy = options[ name ]; // copy = drag (drag方法)
// Prevent never-ending loop
if ( target === copy ) { // 防止自引用 (这里防止将 jQuery(.fn) 添加到 jQuery(.fn) 中)
continue;
}
// Recurse if we're merging plain objects or arrays
if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
// 如果是深拷贝(deep),而且要拷贝的属性值为对象(这里deep = false,不做深拷贝)
if ( copyIsArray ) { // 如果要拷贝的属性值是数组
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src : [];
} else { // 要拷贝的属性值为普通对象
clone = src && jQuery.isPlainObject(src) ? src : {}; // clone 为目标对象中的属性值或空对象
}
// Never move original objects, clone them
target[ name ] = jQuery.extend( deep, clone, copy ); // 递归
// Don't bring in undefined values
} else if ( copy !== undefined ) { // 这里 drag() 非 undefined
target[ name ] = copy; // 将属性添加到目标对象中(这里, jQuery(.fn)[drag] = drag)
}
}
}
}
// Return the modified object
return target; // 返回目标对象(这里返回的是添加了 drag() 方法的 jQuery(.fn))
};