jQuery源码学习笔记(一)

  这是看《犀利开发jQuery内核详解与实践》一书的笔记,看之前讲一下我对jQuery实现的理解:全部封装在一个立即执行匿名函数中,函数开始建立了对重要全局变量的引用。对于全局的$()函数,它会将DOM或者JS对象转换为jQuery对象,转化后的jQuery对象封装在类数组中,当然jQuery对象还挂载了很多方法,jQuery的继承采用prototype机制,为了提高兼容性,内部使用了大量条件判断语句。

一、全局函数jQuery()

jQuery在浏览器中仅建立了一个全局变量——jQuery(),同时它也有个别名$(),接下来就看看这是如何实现的。
首先为了预防和其它库的冲突,jQuery先建立了window下同名变量的引用,最后返回这个变量:

1
2
3
4
5
6
7
8
// 保存原始jQuery的引用,以防被覆盖
_jQuery = window.jQuery,

// 保存原始$的引用,以防被覆盖
_$ = window.$,

// 输出jQuery和$到全局对象
window.jQuery = window.$ = jQuery;

这里要注意noConflict()这个函数,其作用就是向浏览器交回变量$的控制权,这样其它库就可以使用$变量了,传入参数true可以同时交出jQuery()$的控制权,使用方法:$.noConflict();,其它高级用法这里不介绍了,下面是它的源码:

1
2
3
4
5
6
7
8
9
10
noConflict: function( deep ) {
if ( window.$ === jQuery ) {
window.$ = _$; //使$恢复原来的引用
}
//deep为true
if ( deep && window.jQuery === jQuery ) {
window.jQuery = _jQuery; //使jQuery恢复原来的引用
}
return jQuery;
}

二、jQuery对象

将DOM对象用$()包装后就可以得到jQuery对象,jQuery对象不能使用原生的JavaScript方法,DOM对象也不能使用jQuery对象的方法:

1
2
$("h1").innerHTML;  //错误
document.getElementById("id").html(); //错误

接下来看看调用$(document.getElementById("id"))时发生了什么,以窥jQuery对象是如何封装的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 处理传入单个$(DOMElement)的情形
if ( selector.nodeType ) { //nodetype用于判断是不是DOM节点,一般用数字1,2,3等表示不同类型节点
this.context = this[0] = selector; //将上下文context指向传入的DOM对象
this.length = 1; //指定长度为1,
return this;
}

······

//处理传入过个DOM对象,比如document.getElementsbyName
return jQuery.makeArray( selector, this );

······

makeArray: function( array, results ) {
var ret = results || [];

if ( array != null ) {
// The window, strings (and functions) also have 'length'
// Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930
var type = jQuery.type( array );

if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) {
push.call( ret, array );
} else {
jQuery.merge( ret, array ); //将传入的DOM对象数组与ret融合,其实就是一个一个的复制到ret中
}
}

return ret;
}

jQuery对象是类数组(拥有数组索引和长度属性,但没有数组方法,比如arguments)的,它可以用[]访问某一个对象,这个类数组对象通过jQuery.extend扩展了众多方法。

三、DOM加载事件

jQuery中DOM加载事件有多种写法:

  • $(document).ready(handler);
  • $(handler);
  • $().ready(handler);

众所周知,jQuery的DOM加载完成事件不同于window.onload之处在于:事件会在DOM书加载好之后触发,而不是等到所有元素都传输完毕(比如图片加载)。jQuery的具体实现(1.7.2)也不难:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
ready: function( fn ) {
// 附加监听器
jQuery.bindReady();

// Add the callback
readyList.add( fn );

return this;
}

······

bindReady: function() {
if ( readyList ) {
return; //回调列表不为空则直接返回
}

readyList = jQuery.Callbacks( "once memory" );

//处理当调用$(document).ready()时,
//浏览器文档已经加载完(图片也加载完)的情形
if ( document.readyState === "complete" ) {
// Handle it asynchronously to allow scripts the opportunity to delay ready
return setTimeout( jQuery.ready, 1 );
}

// Mozilla, Opera and webkit nightlies currently support this event
if ( document.addEventListener ) {
// DOMContentLoaded在DOM加载完时触发(图片等可能未加载完)
//在这里为此事件添加监听器
//DOMContentLoaded是ready事件的处理函数
document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false );

// A fallback to window.onload, that will always work
window.addEventListener( "load", jQuery.ready, false );
······
}

······

//DOMContentLoaded的定义
DOMContentLoaded = function() {
document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false );
jQuery.ready();
};

······

// Handle when the DOM is ready
ready: function( wait ) {
// 设置一个变量用于记住DOM已经ready了
jQuery.isReady = true;

// 执行readyList中的回调函数
readyList.fireWith( document, [ jQuery ] );

// Trigger any bound ready events
if ( jQuery.fn.trigger ) {
jQuery( document ).trigger( "ready" ).off( "ready" );
}
}

实现的核心在于监听document的DOMContentLoaded事件,并且维持一个readyList的数组用于存放回调函数,等出发时再一次执行。

四、链式调用

JavaScript中实现链式调用比较方便,比如:

1
2
3
4
5
6
7
8
9
10
function Test(){
var b=1;
this.add=function(){
console.log(b++);
return this;
};
}

var obj=new Test();
obj.add().add(); //结果是:1 2

  jQuery中的方法只要返回了自身就能支持链式调用,实际中不是所有的jQuery方法都能支持链式调用,比如eq()会改变当前的执行上下文,但是借助end()方法可以恢复上一个jQuery对象。还有一些方法改变了上下文之后不能恢复原对象,比如get()。下面看一下这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
eq: function( i ) {
i = +i;
return i === -1 ? this.slice( i ) : this.slice( i, i + 1 ); //使用了slice操作
}
······
slice: function() {
return this.pushStack( slice.apply( this, arguments ), //使用了pushStack操作
"slice", slice.call(arguments).join(",") );
}
······
pushStack: function( elems, name, selector ) {
// Build a new jQuery matched element set
var ret = this.constructor();
······
// Add the old object onto the stack (as a reference)
ret.prevObject = this; //此处保存了之前上下文的快照
······
// Return the newly-formed element set
return ret;
}
······
end: function() {
return this.prevObject || this.constructor(null); //返回保存的快照
}

五、jQuery框架

以上都是针对某个具体的特性进行的分析,接下来我们从整体上来学习jQuery的框架。以下是jQuery的基本框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var $=jQuery=function(){  //定义全局对象jQuery和$
//分隔作用域
return new jQuery.fn.init(); //jQuery对象实际上只是初始化构造函数的“增强”
};

jQuery.fn=jQuery.prototype={
constructor: jQuery,
init:function(){ //此处初始化原型中的方法并返回实例的引用
return this; //此处的this和jQuery中的this是不同的
},
jquery:"1.7.2",
size:function(){
return this.length;
}
};
//修改init函数的原型为jQuery的原型,这样就可以用init来实例化jQuery对象了
//并且通过init实例化的对象能够访问jQuery原型链上的方法,实现了跨域访问。
jQuery.fn.init.prototype = jQuery.fn;

console.log($().jquery); //"1.7.2"

巧妙之处在于将jQuery对象原型链上的init函数设置为一个构造函数,这个构造函数可以有自己的方法,同时通过修改它的原型链,通过init实例化的对象也拥有jQuery对象的方法,如此才叫“加强”:完整的jQuery对象是通过init构造的对象加上jQuery原型链方法一起组成的。

六、extend扩展

框架搭好之后就可以为jQuery对象新增各种方法了,在jQuery中扩展是通过extend()函数实现的,这样的好处是不会破坏jQuery框架的原型结构,管理方法更方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
jQuery.extend = jQuery.fn.extend = function() {
var options, name, src, copy, copyIsArray, clone,
target = arguments[0] || {}, //定义复制操作的目标对象
i = 1,
length = arguments.length,
deep = false;

// 深度复制处理
if ( typeof target === "boolean" ) {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
i = 2;
}

// 如果第一个参数是字符串,则设置为空对象
if ( typeof target !== "object" && !jQuery.isFunction(target) ) {
target = {};
}

// 如果只有一个参数,表示把参数对象的方法复制给当前对象
if ( length === i ) {
target = this; //this指向jQuery对象
--i;
}

for ( ; i < length; i++ ) {
// 只处理参数不为null的情况
if ( (options = arguments[ i ]) != null ) {
// 遍历参数对象
for ( name in options ) {
src = target[ name ];
copy = options[ name ];

// 防止死循环
if ( target === copy ) {
continue;
}

// 递归
if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) {
if ( copyIsArray ) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src : [];

} else {
clone = src && jQuery.isPlainObject(src) ? src : {};
}

// Never move original objects, clone them
target[ name ] = jQuery.extend( deep, clone, copy );

// Don't bring in undefined values
} else if ( copy !== undefined ) {
target[ name ] = copy;
}
}
}
}

// Return the modified object
return target;
}

//使用方式:
jQuery.extend();

extend()函数实现的大致思路是:设置一个target对象,第一次时设置target指向this(jQuery对象),之后复制形参的方法,递归调用自身实现复制过程。