注:以下内容为之前积累的 jQuery 源码分析笔记,同步到博客上以作备份。

前言

jQuery是前端开发过程中广泛使用的一个库。据统计,Alex排名前100的网站几乎都直接或间接地使用了jQuery库,由此可见其适用性之广,这也从另一个侧面说明其代码的健壮性。因此,学习和研究jQuery是很有价值的,本文也将作为我学习jQuery的笔记,将持续更新。

最新版的jQuery使用的是符合AMD规范的require.js进行模块式开发,为了方便调试,可以在测试页面加载require.js并设置相应的路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script data-main="./config" src="external/requirejs/require.js"></script>
// config.js
require.config({
baseUrl: "src",
paths: {
"sizzle": "../external/sizzle/dist/sizzle",
"qunit": " ../external/qunit/qunit"
}
});
require(['jquery'], function (jQuery) {
console.log('complete jquery loading');
});

首次使用jQuery选择器的时候,也许会陷入一个认识误区:调用jQuery选择DOM时返回的结果与document.querySelectorAll是一样的。但事实并非如此。jQuery返回的是jQuery对象,其中包含DOM组成的仿数组(即提供this[index]返回DOM的方法)。对于支持querySelector*的浏览器,jQuery选择器引擎会先优先使用querySelector*方法;而对于不支持的浏览器(如IE6、IE7等),才会使用选择器引擎去匹配找出相应的结果。

参考链接

选择器模块

jQuery最核心的一个功能就是可以通过CSS选择器的方式获取DOM结点,其底层使用的是Sizzle选择器引擎。Sizzle选择器的目标很简单:在不支持querySelectorAll的浏览器上模拟该方法的查找过程。注:由于多个并列选择器表达式(由逗号分隔的多个表达式)的情况可以归约到单个选择器表达式的情况,因此下面只讨论单个选择器表达式情形。

在Sizzle中,选择器表达式由块表达式(div.class#id等,对应TAGCLASSID块表达式类型)和块间关系符(>+~)组合形成,另外再外加属性表达式。当得到表达式后,需要将整个表达式进行解析分词,形成一系列的表达式单元。具体而言,它会运用正则表达式匹配来决定每一个块到底是属于什么表达式,分块结果类似如下:(以div.class>p为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// div.class>p
tokens = [{
"value": "div",
"type": "TAG",
"matches": ["div"]
}, {
"value": ".class",
"type": "CLASS",
"matches": ["class"]
}, {
"value": ">",
"type": ">"
}, {
"value": "p",
"type": "TAG",
"matches": ["p"]
}]

得到分词结果以后,很直观的想法就是查找匹配的元素,这个过程有自左向右过滤和自右向左过滤两种方式,而Sizzle使用的从右向左扫描的方法。主要原因还是在于效率:从左向右扫描的话,我们总是需要面对未知数量的后代元素,而从右向左的话,最终结果一定会落在首先得到的元素集合中,之后我们只需要根据选择器表达式进行过滤即可,毕竟父元素或祖先元素总是有限的。

过滤是一个比较复杂的过程,这里用到了一个编译的概念。它会根据选择器表达式和分块结果先进行预编译,即为每个块生成一个相应的匹配器

总的来说,Sizzle选择元素的过程大致如下:

  1. 解析块表达式与块间关系符
  2. 存在伪类,从左向右查找:(注:因为div p:first需要过滤的div p返回结果中的第一个,因此这时应该从左向右,否则就是先查找p:first然后过滤祖先结点为div的元素,显然这是不对的。)
    • 查找第一个块表达式匹配的元素集合
    • 遍历剩余块表达式与块间关系符,缩小上下文元素集合
  3. 否则从右向左查找:
    • 查找最后一个块表达式匹配的元素集合,得到候选集合
    • 遍历剩余块表达式与块间关系符,对候选集合进行过滤
  4. 排序并返回最终匹配的元素(对于并列选择器表达式的情况,依次处理各选择器表达式后还需要对结果进行去重排序)

当然,具体的实现细节可以参考代码中的Sizzle入口函数,通过Sizzle.findSizzle.filter方法查找并过滤元素。当然,Sizzle.findSizzle.filter可以看作是比较高层的方法,而底层的查找实际上还是需要根据块表达式类型的不同使用不同的查找方法,底层的过滤细节也需要根据块间关系及伪类标记的不同而有所不同。这里不详细赘述,只记录一些值得注意的地方。

首先是正则表达式的大量运用。Sizzle有一个属性叫selectors,映射了各类选择器块表达式类型同相应正则表达式(ID,CLASS,NAME,ATTR,TAG,CHILD,POS,PSEUDO)的关系,这样做的好处是方便扩展。块表达式的各种类型对应的正则表达式如下:

1
2
3
4
5
6
7
8
9
10
11
12
Sizzle.selectors = {
match = {
ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/,
NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/,
ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/,
TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/,
CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/,
POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/,
PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/
},
}

此外,为了实现不同类型的表达式调用不同函数,还加入字典成员映射ID,CLASS,NAME,TAG到各自的选择器所需的查找函数,如下:

1
2
3
4
5
6
7
8
Sizzle.selectors = {
find: {
ID: function () { context.getElementById(); },
CLASS: function () { context.getElementsByClassName(); }, // 实际上初始化的时候没有CLASS这一项,需要判断浏览器是否支持getElementsByClassName来决定要不要添加
NAME: function () { context.getElementsByName(); },
TAG: function () { context.getElementsByTagName(); },
}
};

事实上,这种代码形式在Sizzle的filter过滤方法及处理块间关系的时候也是大量地被使用到,可以参考Sizzle.selectors.relative属性和Sizzle.selectors.filter属性。

基础模块

关于jQuery的整体框架结构,我们可以参考它提供的一个sub方法(返回新的jQuery对象,修改这个对象不影响原来的jQ对象):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function sub() {
// 工厂方法
function jQuerySub(selector, context) {
return new jQuerySub.fn.init(selector, context);
}
// 注意这里使用了深度拷贝
jQuery.extend(true, jQuerySub, this);
jQuerySub.superclass = this;
jQuerySub.fn = jQuerySub.prototype = this();
jQuerySub.fn.constructor = jQuerySub;
jQuerySub.sub = this.sub;
jQuerySub.fn.init = function init(selector, context) {
if (context && context instanceof jQuery && !(context instanceof jQuerySub)) {
context = jQuerySub(context);
}
return jQuery.fn.init.call(this, selector, context, rootjQuerySub);
};
jQuerySub.fn.init.prototype = jQuerySub.fn;
var rootjQuerySub = jQuerySub(document);
return jQuerySub;
},

这里需要注意的是jQuery.fn,即jQuery这个工厂函数的原型。它是一个对象:

1
2
3
4
5
6
7
8
9
jQuery.fn = jQuery.prototype = {
constructor: jQuery, // constructor指回jQuery工厂函数
init: function () {}, // jQuery最核心的选择器匹配函数
selector: "",
jquery: '1.7.1',
each: function () {},
ready: function (fn) {},
...
};

从源码中,我们可以看到,jQuery这个工厂函数实际上最终也是调用了jQuery.prototype.init这个方法实例出来的一个新对象,所以jQuery.prototype.init可以看作一个构造函数,它的原型也是指向jQuery.prototype,本质上,这种写法可以简化为如下的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
!function (global) {
function jQ() {
return new jQ.prototype.init();
};
var fn = jQ.prototype,
init;
init = fn.init = function () {
this.value = Math.random();
return this;
}
init.prototype = fn;
global.jQ = jQ;
}(global);
var obj = jQ();
console.log(obj, jQ());
console.log(jQ.prototype);

jQuery提供了一个each方法,可以对对象每一个属性成员都调用同一个回调函数callback.apply()

为了实现多层对象的继承,jQuery提供extend这个API。其接口为:

1
2
3
// 支持深度拷贝
// 如果参数只有一个,则是扩展jQuery本身,否则扩展传入的目标对象
$.extend([deep], target, [object1], [object2], [...]);

这里如果提供的参数包含深度拷贝,则需要使用递归的方法扩展其中的某些属性(处理对象Object和数组Array两种情形)。

有了extend这个方法,对jQuery本身(或其原型)的扩展就变得非常地方便了:

1
2
3
4
5
jQuery.extend({
prop: value,
fun: function () {}
});
jQuery.fn.extend({});

jQuery(document).ready | jQuery(fn)dom.onload区别在于,一旦DOM构建好以后,ready事件就可以被触发了,而onload只会在所有DOM及脚本图像等资源处理好以后才开始。jQuery是怎么实现的呢?对于现代浏览器,它直接利用了document有一个DOMContentLoaded事件,一旦DOM构建好自动触发对应回调,只需要在回调中加入ready相应的函数即可。(注:由于高版本jQuery只支持IE9+,所以它只利用了document.readyState === "complete"DOMContentLoaded,但对于不支持这些方法的低版本的IE浏览器,需要不断地定时判断一个scroll相关的方法是否可用。高版本的jQuery已改用promise来实现ready相关的内容)。

此外,为了确定浏览器的版本信息,jQuery使用了正则表达式进行处理以匹配出合适的浏览器内核名称、版本等相关信息。PS:对navigator.userAgent字符串进行解析就可以获得这些信息。

事件系统

事件是JS里最重要的一个概念, jQuery对各浏览器实现的事件重新进行了一层封装,一方面使得处理更加统一,另一方面支持更广义的自定义事件。

一般的事件包括注册、触发、解除事件。jQuery的事件注册有很多种方法,不过底层调用的是统一的on方法。其注册事件包括:

1
2
3
$('selector').on('click', 'p', function (evt) {});
$ ('selector').click('p', function (evt) {});
$('selector').delegate('p', 'click', function (evt) {});

需要说明的是,on方法是暴露给外面的最底层方法,不过在其内部,实质上还是调用了$.event.add方法添加回调函数,on里面处理的主要是各种参数的调整(因为JS语法不支持默认参数,所以只能通过判断各参数是否undefined来判断并重新赋值)。另外就是jQuery支持once这种方法,即事件只会被触发一次,这一点也是在on方法中实现的。这一点也很容易实现,即对自定义的回调进行包装,使之调用后解除注册即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
if ( one === 1 ) {
origFn = fn;
fn = function( event ) {
// Can use an empty set, since event contains the info
jQuery().off( event );
return origFn.apply( this, arguments );
};
// Use same guid so caller can remove using origFn
fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ );
}
return this.each( function() { // 对匹配的每个DOM都注册事件
jQuery.event.add( this, types, fn, data, selector );
});

所以,更重要的,我们需要分析jQuery.event.add方法。它主要是将注册函数与DOM本身解耦,将事件相关的内容存储到缓存系统中( 缓存系统中供jQuery内部使用的dataPriv对象),每个结点都会在dataPriv.cache中有一份关联的数据 elemData,存储所有相关的数据,事件相关的内容存放在elemData.events中。另外还有一点需要注意的是,jQuery并没有把我们自定义的回调函数直接作为事件触发回调,而是对进行了一层抽象,保存到elemData.handle中,然后把这个抽象的回调函数elemData.handle添加到事件注册列表中:

1
2
3
4
5
6
7
8
9
if (!(eventHandle = elemData.handle)) {
// 实际上jQuery内部的callback对传入的callback还进行了包装
eventHandle = elemData.handle = function(e) {
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== "undefined" && jQuery.event.triggered !== e.type ?
jQuery.event.dispatch.apply(elem, arguments) : undefined;
};
}

elemData.handle会在这个节点任意事件触发的时候调用到,然后$.event.dispatch会根据事件的类型到elemData.events中查找相应的关联数据(这包括事件、我们自定义的回调、选择器等等信息)。 值得注意的一点在于,在jQuery.event.dispatch中,它会先对传入的系统event进行一层重新包装(修改)生成一个$.Event对象,屏蔽掉各浏览器之间的差异(如IE的event是一个全局对象,而FF和chrome则是触发时产生的对象)。获得了相应的回调函数列表以后,就可以将它们放入队列,并根据它们对应的选择器依次决定是否需要执行。

TODO:注意,dispatch更多地是应用于内部的调用,而真正我们希望自定义其触发行为的时候,可能更有用的是trigger方法,它底层也是调用了$.event.trigger方法。

异步队列

异步队列模块(Deferred)在jQuery中主要用于实现异步任务与回调函数的解耦,为其它模块提供了基础,它基于Callbacks实现。先了解一个简单的异步队列使用情形:

传统上在jQuery中发一个ajax请求代码如下:

1
2
3
4
5
6
7
8
9
10
11
$.ajax({
url: "/echo/json/",
data: {json: JSON.stringify({firstName: "Jose", lastName: "Romaniello"})} ,
type: "POST",
success: function(person){
alert(person.firstName + " saved.");
},
error: function(){
alert("error!");
}
});

其中使用了两个回调,一个在请求成功返回后将被调用,而另一个则是处理错误发生的情况。但是,使用异步队列后,上面的请求就可以很简单地写成:

1
2
3
4
5
6
7
8
// $.ajax返回的是一个具有Promise接口的对象
$.ajax({
url: "/echo/json",
data: {json: JSON.stringify({firstName: "Jose", lastName: "Romaniello"})},
type: "POST",
}).done(function (person) { alert(person.firstName + " saved."); } )
.done(function () { console.log("成功返回"); })
.fail(function () { alert("error!"); });

这里用到的就是Deferred异步处理模块,在介绍它之前,先大致地描述一下jQuery.Callbacks,这是Deferred实现的基础。

Callback实现的是管理一系列回调函数,它是一个工厂方法,每次调用jQuery.Callbacks()都会返回一个根据给定参数生成的管理对象。它支持的参数包括:

  • memory:调用fire或fireWith以后,它会记住传入的参数,下次再调用add添加回调的时候,就自动利用这些参数触发回调了。(记住的是最后一次调用fire或fireWith的参数)
  • once:保证回调只执行一次,执行完回调函数以后,调用disable方法,即将回调列表清除。
  • unique:一个回调函数只能添加一次
  • stopOnFalse:因为它管理的是一系列回调,所以执行是依次执行的,如果有一个执行失败,则后面的都不再执行,直接退出。

TODO:具体的实现细节分析。

事实上,Deferred模块提供了三种状态的回调列表:resolved、rejected和progress状态(每种状态对应一个回调队列Callbacks实例,参数为”once memory”)。每种状态的回调队列都有各自的添加回调函数的方法,分别是done、fail和progress,实际上它们都是Callbacks的add方法的一个包装。当然,jQuery还提供了一个always方法用于添加回调函数,实际上就是done和fail都会添加这个回调,这样看来还是调用了done和fail。然后jQuery提供了三个执行(触发)函数:resolve、reject和notify(当然,还提供了对应的With函数用于设定自定义的上下文)。对于这三种情况,jQuery的实现还是很值得学习的:

1
2
3
4
5
6
7
8
// tuples定义一个数组,包含三种状态的触发函数名、回调函数添加方法、异步队列及状态名称
// 这样接下来对三种情况的处理就可以放在一个forEach里了
var tuples = [
// action, add listener, listener list, final state
[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
[ "notify", "progress", jQuery.Callbacks("memory") ]
],

对于Deferred的实现,jQuery通过复制了一个只读的副本promise的方法来扩展Deferred的方法,从而使得自身具有Promise属性。TODO:Promise可以看作是将异步回调嵌套线性化的一种标准,有机会将深入研究。promise提供了一个叫promise的方法,通过传入一个对象,会将promise所拥有的方法扩展到该对象上,Deferred对象的实现就是如此(promise.promise(deferred))。

由于其它方法比较简单,这里只谈一下promise.then方法,实际上它在低版本中的方法名为pipe,用于返回新的异步队列的只读副本,通过过滤函数过滤当前异步队列的状态及值:

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
then: function( /* fnDone, fnFail, fnProgress */ ) {
// 1. 重新创建新的Deferred对象
// 2. 向老的Deferred对象(this)添加包装了的函数
// 3. 在这个包装了的函数中:
// - 先调用我们一开始传入的fnDone | fnFail | fnProgress
// - 如果返回的是一个promise对象,将向这个promise对象中调用done/fail/progress来分别添加下一个then中的参数
// - 否则,触发设置resolved状态
var fns = arguments;
return jQuery.Deferred(function( newDefer ) {
jQuery.each( tuples, function( i, tuple ) {
var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ];
// deferred[ done | fail | progress ] for forwarding actions to newDefer
deferred[ tuple[1] ](function() { // 这里是向老的Deferred对象添加包装的回调(即 2 )
var returned = fn && fn.apply( this, arguments );
if ( returned && jQuery.isFunction( returned.promise ) ) {
returned.promise()
.done( newDefer.resolve )
.fail( newDefer.reject )
.progress( newDefer.notify );
} else {
newDefer[ tuple[ 0 ] + "With" ](
this === promise ? newDefer.promise() : this,
fn ? [ returned ] : arguments
);
}
});
});
fns = null;
}).promise();
},

使用示例:

1
2
3
4
5
6
7
8
9
10
var defer = $.Deferred(); // 返回Deferred对象
setTimeout(function () {
defer.resolve('HI'); // 触发成功的回调 | 即后面添加的回调函数
}, 50);
defer.then(function (msg) { // 向defer的回调列表中添加回调函数(传入的函数作一定的包装以后再添加),同时会返回一个新的promise对象,用于链式调用then
console.log(msg);
});
defer.done(function (msg) { // 直接向defer回调列表添加我们自定义的函数
console.log('I came from ' + msg);
});

===UPDATE:简化版的Deferred===

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
// then的链式调用如下:
// 1. 如果添加的函数返回的是Promise对象,就需要将这个promise的done与下一个promise的resolve(newDefer.resolve)串联起来
// 2. 如果返回的是普通的对象,就立即执行newPromise内的回调。即newPromise.resolveWith(newDefer.promise(), fn ? [ret] : arguments);
// var newProm = promise.then(function () {
// return val;
// });
$.Deferred = function (fnWrap) {
var defer = {};
var promise = {
then: function (fnDone, fnFail, fnProgress) {
return $.Deferred(fuction (newDefer) {
// 对fnXXX,分别串连新老Defer之间的关系,使得在老的defer完成的时候,能够调用新的newDefer的触发函数。以fnDone为例:
defer.done(function () {
var ret = (typeof fnDone === 'function') && fnDone.apply(this, arguments);
if (ret && typeof ret.promise === 'function') { // 返回的是一个Promise对象,就需要在这个新的Promise完成时触发newDefer的回调
ret.promise()
.done(newDefer.resolve)
.fail(newDefer.reject)
.progress(newDefer.notify);
} else { // 否则直接触发newDefer的对应回调
newDefer.resolveWith(defer.promise(), fnDone ? [ret]);
}
});
}).promise(); // return the promise
},
promise: function (obj) {
return obj ? $.extend(defer, promise) : promise;
}
};
if (fnWrap) {
fnWrap.call(defer, defer);
}
return defer;
}

以上就是异步队列模块的全部内容。

数据缓存

为了实现对DOM及其关联的数据解耦和避免内存的泄漏,jQuery实现了数据缓存模块。首先,它并不会直接把数据加到DOM上,而是在一开始设置数据的时候将数据关联到一个Data对象上,专门用于存储数据。所有的数据对象都是存储在Data对象的cache属性中,通过一个唯一的key映射到每一个具体的对象上。在jQuery中,内部使用的是dataPriv,而供外部使用的则是dataUser,它们都是Data对象。

下面先介绍Data这个构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// $.data中所涉及到的都是这个Data构造的对象
function Data() {
this.expando = jQuery.expando + Math.random();
this.cache = {}; // 为避免外部枚举到,它使用Object.defineProperty定义了cache成员变量
}
// 真实的数据其实存放在cache上,然后key标识一个待关联的对象,用于获取一个cache中标识的ID,与前面的全局唯一ID不一样(因为有可能多次设置同一个属性的值)
Date.prototype = {
// owner即关联的对象,如DOM结点等,owner[this.expando]即这个对象在cache中的id
key: function (owner) {},
set: function (owner, data, value) {},
get: function (owner, key) {}, //
// 如果没有key和value,返回所有的owner相关的数据;如果没有value,则实际上调用的是get,否则调用的是set
access: function (owner, key, value) {}
};
var dataUser= new Data(); // 每次调用$.data实际上都是向userData的cache属性中设值
var dataPriv= new Data();

jQuery提供了两套data接口,一个是jQuery.data,另一个则是jQuery对象的data方法。jQuery.data定义如下:

1
2
3
jQuery.data = function (elem, name, data) {
dataUser.access( elem, name, data );
}

jQuery.fn.data是对jQuery对象的每一个DOM结点进行data操作,还需要考虑HTML的data-name这样的标签,它的定义稍微复杂一点。

1
2
3
4
5
6
7
8
9
jQuery.fn.data = function (key, value) {
if (key === undefined) {
// 取第一个DOM元素并返回它的所有data,其中如果这个DOM本身有data-name这样的H5标签,还会先把它写入到dataUser中进行缓存
}
if (typeof key === 'object') {
// 对每一个DOM元素都设置相应的值
}
// 最后一种情况是对每一个元素取或设值,调用的是基础的access函数 + 自定义函数参数。
}

由前面的分析,我们不难看出,jQuery.data是将 $('')得到的结果当一个整体,而 jQuery.fn.data 则是将每一个DOM设置各自的数据。所以下面的输出是正常的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 因为containerA和containerB是两个不同的jQuery对象,所以关联的数据自然不同
// 但containerA.data 和 containerB.data 都是对DOM进行关联数据,自然会出现覆盖。
// 一般情况下,我们可能更需要用的是$('').data()
var containerA = $('.container'),
containerB = $('.container');
containerA.data({
name: 'Jiaxiang Zheng'
});
containerB.data({
name: 'Jiaxiang Zheng - UPDATE'
});
console.log(containerA.data('name')); // Jiaxiang Zheng - UPDATE
console.log(containerB.data('name')); // Jiaxiang Zheng - UPDATE
$.data(containerA, {
name: 'A'
});
$.data(containerB, {
name: 'B'
});
console.log($.data(containerA, 'name')); // A
console.log($.data(containerB, 'name')); // B

至此,整个的数据缓存模块就分析完成了。

属性操作

jQuery的属性操作包括HTML属性操作(attr)、DOM属性操作(prop)、CSS操作(class)和值操作(val)。总体上说,jQuery.prototypejQuery都包含了同名的各操作方法,但它们之间的关系是(以attr为例):jQuery.fn.attr会通过access方法作一定的检查,而后再调用到jQuery.attr处理具体的问题。所以本质上,主要的实现是在jQuery相关的方法中。

1
2
3
4
jQuery.fn.attr = function (key, value) {
// 最后一个参数表示不是get操作就返回jQuery对象
return access(this, jQuery.attr, key, value, arguments.length > 1);
}

我们分析一下jQuery.access方法:

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
// 处理每一个元素 | 取元素相应属性 | 设置元素相应属性
var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) {
var i = 0,
len = elems.length,
bulk = key == null;
// Sets many values
if ( jQuery.type( key ) === "object" ) {
chainable = true;
for ( i in key ) {
access( elems, fn, i, key[i], true, emptyGet, raw );
}
// Sets one value
} else if ( value !== undefined ) {
chainable = true;
if ( !jQuery.isFunction( value ) ) {
raw = true;
}
if ( bulk ) {
// Bulk operations run against the entire set
if ( raw ) {
fn.call( elems, value );
fn = null;
// ...except when executing function values
} else {
bulk = fn;
fn = function( elem, key, value ) {
return bulk.call( jQuery( elem ), value );
};
}
}
if ( fn ) {
for ( ; i < len; i++ ) {
fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) );
}
}
}
return chainable ?
elems :
// Gets
bulk ?
fn.call( elems ) :
len ? fn( elems[0], key ) : emptyGet;
};

有了access方法以后,jQuery.fn.attr就可以通过access方法处理每一个元素,而具体的处理方法由jQuery.attr实现。jQuery.fn.attr方法也是如此实现的。具体的jQuery.attr对每一个DOM结点进行操作,为了屏蔽掉不同浏览器和自定义的一些设置,jQuery使用钩子函数,如果在attrHooks中没有找到相应的钩子,就使用默认的getAttribute / setAttribute

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 其它几个方法对应的Hooks分别为propFix, propHooks, valHooks等
attrHooks = {
type: {
},
borderRadius: {
set: function (elem, value) {
},
get: function (elem) {
}
}
}
boolHooks = {
}

AJAX 模块

jQuery对外提供了好几个AJAX请求的接口,如$.get()$.post()等,但它们的底层还是调用了$.ajax这个函数。

TODO