# jQuery 整体架构图
# jQuery 闭包
jQuery 具体的实现,都被包含在了一个立即执行函数构造的闭包里面,为了不污染全局作用域,只在后面暴露 $
和 jQuery
这 2 个变量给外界,尽量的避开变量冲突。
(function(window, undefined) {
// jQuery 代码
})(window);
2
3
闭包常用的还有另一种写法:
(function(window) {
// js 代码
})(window, undefined);
2
3
比较推崇的的第一种写法,也就是 jQuery 的写法。
二者的不同之处在于,当我们的代码运行在更早期的环境当中,undefined 仅是一个变量且它的值是可以被覆盖的。意味着可以做这样的操作:
undefined = 42;
console.log(undefined); // 42
2
当使用第一种方式,可以确保你需要的 undefined 确实就是 undefined。
这里还有一点要注意的是,jQuery 在这里有一个针对压缩优化细节,使用第一种方式,在代码压缩的时候,window 和 undefined 都可以压缩为 1 个字母并且确保它们就是 window 和 undefined。
// w -> window, u -> undefined
(function(w, u) {
// jQuery 代码
})(window);
2
3
4
# jQuery 不需要 new
使用 jQuery 的时候,实例化一个 jQuery 对象的方法如下:
// 无 new 构造
$("#test").text("Test");
// 当然也可以使用 new
var test = new $("#test");
test.text("Test");
2
3
4
5
6
大部分人使用 jQuery 的时候都是使用第一种无 new 的构造方式,直接 $('') 进行构造,这也是 jQuery 十分便捷的一个地方。
当我们使用第一种无 new 构造方式的时候,其本质就是相当于 new jQuery(),那么在 jQuery 内部是如何实现的呢?
(function(window, undefined) {
var
// ...
jQuery = function(selector, context) {
// 实例化方法 jQuery() 实际上是调用了其拓展的原型方法 jQuery.fn.init
return new jQuery.fn.init(selector, context, rootjQuery);
},
// jQuery.prototype 即是 jQuery 的原型,挂载在上面的方法,即可让所有生成的 jQuery 对象使用
jQuery.fn = jQuery.prototype = {
// 实例化化方法,这个方法可以称作 jQuery 对象构造器
init: function(selector, context, rootjQuery) {
// ...
}
}
// 这一句很关键,也很绕
// jQuery 没有使用 new 运算符将 jQuery 实例化,而是直接调用其函数
// 要实现这样,那么 jQuery 就要看成一个类,且返回一个正确的实例
// 且实例还要能正确访问 jQuery 类原型上的属性与方法
// jQuery 的方式是通过原型传递解决问题,把 jQuery 的原型传递给jQuery.fn.init.prototype
// 所以通过这个方法生成的实例 this 所指向的仍然是 jQuery.fn,所以能正确访问 jQuery 类原型上的属性与方法
jQuery.fn.init.prototype = jQuery.fn;
})(window);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
理解以上这段代码有几个关键点:
首先要明确,使用 $('xxx') 这种实例化方式,其内部调用的是
return new jQuery.fn.init(selector, context, rootjQuery)
这一句话,也就是构造实例是交给了 jQuery.fn.init() 方法去完成。将 jQuery.fn.init 的 prototype 属性设置为 jQuery.fn,那么使用 new jQuery.fn.init() 生成的对象的原型对象就是 jQuery.fn ,所以挂载到 jQuery.fn 上面的函数就相当于挂载到 jQuery.fn.init() 生成的 jQuery 对象上,所有使用 new jQuery.fn.init() 生成的对象也能够访问到 jQuery.fn 上的所有原型方法。
也就是实例化方法存在这么一个关系链:
jQuery.fn.init.prototype = jQuery.fn = jQuery.prototype;
new jQuery.fn.init() 相当于 new jQuery();
jQuery() 返回的是 new jQuery.fn.init(),而 var obj = new jQuery(),所以这 2 者是相当的,所以我们可以不用 new 的方式来实例化 jQuery 对象。
2
3
4
5
# jQuery 方法的重载
jQuery 源码晦涩难读的另一个原因是,使用了大量的方法重载,但是用起来却很方便:
// 获取 title 属性的值
$("#id").attr("title");
// 设置 title 属性的值
$("#id").attr("title", "jQuery");
// 获取 css 某个属性的值
$("#id").css("title");
// 设置 css 某个属性的值
$("#id").css("width", "200px");
2
3
4
5
6
7
8
9
方法的重载就是指一个方法实现多种功能。大多数人使用 jQuery() 构造方法使用的最多的就是直接实例化一个 jQuery 对象,但其实在它的内部实现中,有着 9 种不同的方法重载场景:
// 接受一个字符串,其中包含了用于匹配元素集合的 CSS 选择器
jQuery([selector,[context]])
// 传入单个 DOM
jQuery(element)
// 传入 DOM 数组
jQuery(elementArray)
// 传入 JS 对象
jQuery(object)
// 传入 jQuery 对象
jQuery(jQuery object)
// 传入原始 HTML 的字符串来创建 DOM 元素
jQuery(html,[ownerDocument])
jQuery(html,[attributes])
// 传入空参数
jQuery()
// 绑定一个在 DOM 文档载入完成后执行的函数
jQuery(callback)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
所以读源码的时候,很重要的一点是结合 jQuery API 进行阅读,去了解方法重载了多少种功能。
# jQuery.fn.extend 与 jQuery.extend
extend 方法在 jQuery 中是一个很重要的方法,jQuey 内部用它来扩展静态方法或实例方法,而且我们开发 jQuery 插件开发的时候也会用到它。但是在内部,是存在 jQuery.fn.extend
和 jQuery.extend
两个 extend 方法的,而区分这两个 extend 方法是理解 jQuery 的很关键的一部分。
jQuery.extend(object) 是为扩展 jQuery 类本身,为类添加新的静态方法;
jQuery.fn.extend(object) 是给 jQuery 对象添加实例方法,也就是通过这个 extend 添加的新方法,实例化的 jQuery 对象都能使用,因为它是挂载在 jQuery.fn 上的方法(jQuery.fn = jQuery.prototype )。
它们的官方解释是:
jQuery.extend(): 把两个或者更多的对象合并到第一个当中。
jQuery.fn.extend():把对象挂载到 jQuery 的 prototype 属性,来扩展一个新的 jQuery 实例方法。
也就是说,
使用 jQuery.extend() 拓展的静态方法,我们可以直接使用 $.xxx
进行调用(xxx 是拓展的方法名),而使用 jQuery.fn.extend() 拓展的实例方法,需要使用 $().xxx
调用。
源码如下:
// 扩展合并函数
// 合并两个或更多对象的属性到第一个对象中,jQuery 后续的大部分功能都通过该函数扩展
// 虽然实现方式一样,但是要注意区分用法的不一样,那么为什么两个方法指向同一个函数实现,但是却实现不同的功能呢,
// 阅读源码就能发现这归功于 this 的强大力量
// 如果传入两个或多个对象,所有对象的属性会被添加到第一个对象 target
// 如果只传入一个对象,则将对象的属性添加到 jQuery 对象中,也就是添加静态方法
// 用这种方式,我们可以为 jQuery 命名空间增加新的方法,可以用于编写 jQuery 插件
// 如果不想改变传入的对象,可以传入一个空对象:$.extend({}, object1, object2);
// 默认合并操作是不迭代的,即便 target 的某个属性是对象或属性,也会被完全覆盖而不是合并
// 如果第一个参数是 true,则是深拷贝
// 从 object 原型继承的属性会被拷贝,值为 undefined 的属性不会被拷贝
// 因为性能原因,JavaScript 自带类型的属性不会合并
jQuery.extend = jQuery.fn.extend = function() {
var src,
copyIsArray,
copy,
name,
options,
clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false;
// Handle a deep copy situation
// target 是传入的第一个参数
// 如果第一个参数是布尔类型,则表示是否要深递归,
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
// skip the boolean and the target
// 如果传了类型为 boolean 的第一个参数,i 则从 2 开始
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
// 如果参数的长度为 1 ,表示是 jQuery 静态方法
if (length === i) {
target = this;
--i;
}
// 可以传入多个复制源
// i 是从 1或2 开始的
for (; i < length; i++) {
// Only deal with non-null/undefined values
// 将每个源的属性全部复制到 target 上
if ((options = arguments[i]) != null) {
// Extend the base object
for (name in options) {
// src 是源(即本身)的值
// copy 是即将要复制过去的值
src = target[name];
copy = options[name];
// Prevent never-ending loop
// 防止有环,例如 extend(true, target, {'target':target});
if (target === copy) {
continue;
}
// Recurse if we're merging plain objects or arrays
// 这里是递归调用,最终都会到下面的 else if 分支
// jQuery.isPlainObject 用于测试是否为纯粹的对象
// 纯粹的对象指的是 通过 "{}" 或者 "new Object" 创建的
// 如果是深复制
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
// 返回新的 target
// 如果 i < length ,是直接返回没经过处理的 target,也就是 arguments[0]
// 也就是如果不传需要覆盖的源,调用 $.extend 其实是增加 jQuery 的静态方法
return target;
};
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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
需要注意的是这一句 jQuery.extend = jQuery.fn.extend = function() {}
,也就是 jQuery.extend 的实现和 jQuery.fn.extend 的实现共用了同一个方法,但是为什么能够实现不同的功能了,这就要归功于 Javascript 的 this 了。
在 jQuery.extend() 中,this 的指向是 jQuery 对象(或者说是 jQuery 类),所以这里扩展在 jQuery 上。
在 jQuery.fn.extend() 中,this 的指向是 fn 对象,而 jQuery.fn = jQuery.prototype ,也就是这里增加的是原型方法,也就是对象方法。
# jQuery 的链式调用及回溯
jQuery 的链式调用实现很简单,只需要在要实现链式调用的方法的返回结果里返回 this,就能够实现链式调用了。
除了链式调用,jQuery 甚至还允许回溯,比如:
// 通过 end() 方法终止在当前链的最新过滤操作,返回上一个对象集合
$("div")
.eq(0)
.show()
.end()
.eq(1)
.hide();
2
3
4
5
6
7
当选择了 ('div').eq(0) 之后使用 end() 可以回溯到上一步选中的 jQuery 对象 $('div'),其内部实现其实是依靠添加了 prevObject
这个属性,prevObject 保存了上一步的 jQuery 对象集合。
jQuery 完整的链式调用、增栈、回溯通过 return this
、 return this.pushStack()
、return this.prevObject
实现,源码实现如下:
jQuery.fn = jQuery.prototype = {
// 将一个 DOM 元素集合加入到 jQuery 栈
// 此方法在 jQuery 的 DOM 操作中被频繁的使用, 如在 parent(), find(), filter() 中
// pushStack() 方法通过改变一个 jQuery 对象的 prevObject 属性来跟踪链式调用中前一个方法返回的 DOM 结果集合
// 当我们在链式调用 end() 方法后, 内部就返回当前 jQuery 对象的 prevObject 属性
pushStack: function(elems) {
// 构建一个新的jQuery对象,无参的 this.constructor(),只是返回引用this
// jQuery.merge 把 elems 节点合并到新的 jQuery 对象
// this.constructor 就是 jQuery 的构造函数 jQuery.fn.init,所以 this.constructor() 返回一个 jQuery 对象
// 由于 jQuery.merge 函数返回的对象是第二个函数附加到第一个上面,所以 ret 也是一个 jQuery 对象,这里可以解释为什么 pushStack 出入的 DOM 对象也可以用 CSS 方法进行操作
var ret = jQuery.merge(this.constructor(), elems);
// 给返回的新 jQuery 对象添加属性 prevObject
// 所以也就是为什么通过 prevObject 能取到上一个合集的引用了
ret.prevObject = this;
ret.context = this.context;
// Return the newly-formed element set
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]] : []);
},
};
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
总结如下:
end() 方法返回 prevObject 属性,这个属性记录了上一步操作的 jQuery 对象合集;
而 prevObject 属性由 pushStack() 方法生成,该方法将一个 DOM 元素集合加入到 jQuery 内部管理的一个栈中,通过改变 jQuery 对象的 prevObject 属性来跟踪链式调用中前一个方法返回的 DOM 结果集合;
当我们在链式调用 end() 方法后,内部就返回当前 jQuery 对象的 prevObject 属性,完成回溯。
# jQuery 变量冲突处理
jQuery 一开始保存全局变量的 window.jQuery
以及 window.$
,当需要处理冲突的时候,调用静态方法 noConflict()
,让出变量的控制权,源码如下:
(function(window, undefined) {
var
// Map over jQuery in case of overwrite
// 设置别名,通过两个私有变量映射了 window 环境下的 jQuery 和 $ 两个对象,以防止变量被强行覆盖
_jQuery = window.jQuery,
_$ = window.$;
jQuery.extend({
// noConflict() 方法让出变量 $ 的 jQuery 控制权,这样其他脚本就可以使用它了
// 通过全名替代简写的方式来使用 jQuery
// deep -- 布尔值,指示是否允许彻底将 jQuery 变量还原(移交 $ 引用的同时是否移交 jQuery 对象本身)
noConflict: function(deep) {
// 判断全局 $ 变量是否等于 jQuery 变量
// 如果等于,则重新还原全局变量 $ 为 jQuery 运行之前的变量(存储在内部变量 _$ 中)
if (window.$ === jQuery) {
// 此时 jQuery 别名 $ 失效
window.$ = _$;
}
// 当开启深度冲突处理并且全局变量 jQuery 等于内部 jQuery,则把全局 jQuery 还原成之前的状况
if (deep && window.jQuery === jQuery) {
// 如果 deep 为 true,此时 jQuery 失效
window.jQuery = _jQuery;
}
// 这里返回的是 jQuery 库内部的 jQuery 构造函数(new jQuery.fn.init())
// 像使用 $ 一样尽情使用它吧
return jQuery;
}
})
}(window)
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
简单的流程图如下:
让出这两个符号之后,还是能在我们的代码中使用 jQuery 或者 $ 的:
var query = jQuery.noConflict(true);
(function($) {
// 插件或其他形式的代码,也可以将参数设为 jQuery
})(query);
// 其他用 $ 作为别名的库的代码
2
3
4
5
6
7
# 短路表达式与多重短路表达式
在 jQuery 中,大量的使用了短路表达式与多重短路表达式。
短路表达式:作为 &&
和 ||
操作符的操作数表达式,这些表达式在进行求值时,只要最终的结果已经可以确定是真或假,求值过程便告终止,这称之为短路求值。
// || 短路表达式
var foo = a || b;
// 相当于
if (a) {
foo = a;
} else {
foo = b;
}
// && 短路表达式
var bar = a && b;
// 相当于
if (a) {
bar = b;
} else {
bar = a;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这两个例子是短路表达式最简单是情况,多数情况下,jQuery 是这样使用它们的:
// 选自 jQuery 源码中的 Sizzle 部分
function siblingCheck(a, b) {
var cur = b && a,
diff =
cur &&
a.nodeType === 1 &&
b.nodeType === 1 &&
(~b.sourceIndex || MAX_NEGATIVE) - (~a.sourceIndex || MAX_NEGATIVE);
// other code ...
}
2
3
4
5
6
7
8
9
10
11
diff 的值经历了多重短路表达式配合一些全等判断才得出,这种代码很优雅,但是可读性下降了很多,使用的时候权衡一下。
多重短路表达式和简单短路表达式其实一样,只需要先把后面的当成一个整体,依次推进,得出最终值。
var a = 1,
b = 0,
c = 3;
var foo = a && b && c, // 0,相当于 a && (b && c)
bar = a || b || c; // 1
2
3
4
5
6
有几个需要注意的点:
在 Javascript 的逻辑运算中,
0、''、null、false、undefined、NaN
都会判定为 false ,而其他都为 true;因为 Javascript 的内置弱类型域 (weak-typing domain),所以对严格的输入验证这一点不太在意,即便使用 && 或者 || 运算符的运算数不是布尔值,仍然可以将它看作布尔运算。虽然如此,还是建议如下:
if (foo) {
} //不够严谨
if (!!foo) {
} //更为严谨,!!可将其他类型的值转换为 boolean 类型
2
3
4
# 预定义常用方法的入口
在 jQuery 的头几十行,有这么一段有趣的代码:
(function(window, undefined) {
var // 定义了一个对象变量,一个字符串变量,一个数组变量
class2type = {},
core_version = "1.10.2",
core_deletedIds = [],
// 保存了对象、字符串、数组的一些常用方法 concat push 等等...
core_concat = core_deletedIds.concat,
core_push = core_deletedIds.push,
core_slice = core_deletedIds.slice,
core_indexOf = core_deletedIds.indexOf,
core_toString = class2type.toString,
core_hasOwn = class2type.hasOwnProperty,
core_trim = core_version.trim;
})(window);
2
3
4
5
6
7
8
9
10
11
12
13
14
首先这里定义了一个对象变量、一个字符串变量、数组变量,要注意这 3 个变量本身在下文是有自己的用途的;其次,借用这三个变量,再定义些常用的核心方法,从上往下是数组的 concat、push、slice、indexOf 方法,对象的 toString、hasOwnProperty 方法以及字符串的 trim 方法,core_xxxx 这几个变量事先存储好了这些常用方法的入口,如果下文行文当中需要调用这些方法,可以这么做:
jQuery.fn = jQuery.prototype = {
// ...
// 将 jQuery 对象转换成数组类型
toArray: function() {
// 调用数组的 slice 方法,使用预先定义好了的 core_slice,节省查找内存地址时间,提高效率
// 相当于 return Array.prototype.slice.call(this)
return core_slice.call(this);
},
};
2
3
4
5
6
7
8
9
可以看到,当需要使用这些预先定义好的方法,只需要借助 call 或者 apply(详解 (opens new window))进行调用。
jQuery 这样做的好处有哪些呢?
以数组对象的 concat 方法为例,如果不预先定义好 core_concat = core_deletedIds.concat 而是调用实例 arr 的方法 concat 时,首先需要辨别当前实例 arr 的类型是 Array,在内存空间中寻找 Array 的 concat 内存入口,把当前对象 arr 的指针和其他参数压入栈,跳转到 concat 地址开始执行,而当保存了 concat 方法的入口 core_concat 时,完全就可以省去前面两个步骤,从而提升一些性能;
借助 call 或者 apply 的方式调用,让一些类数组可以直接调用数组的方法。就如上面示例,jQuery 对象是类数组类型,可以直接调用数组的 slice 方法转换为数组类型。又譬如,将参数 arguments 转换为数组类型:
function test(a, b, c) {
// 将参数 arguments 转换为数组
// 使之可以调用数组成员方法
var arr = Array.prototype.slice.call(arguments);
// ...
}
2
3
4
5
6
# 钩子机制(hook)
在 jQuery 2.0.0 之前的版本,对兼容性做了大量的处理,正是这样才让广大开发人员能够忽略不同浏览器的不同特性的专注于业务本身的逻辑。而其中,钩子机制在浏览器兼容方面起了十分巨大的作用
钩子是编程惯用的一种手法,用来解决一种或多种特殊情况的处理。
简单来说,钩子就是适配器原理,或者说是表驱动原理,我们预先定义了一些钩子,在正常的代码逻辑中使用钩子去适配一些特殊的属性,样式或事件,这样可以让我们少写很多 else if 语句。
jQuery 的 hook 是一种更为抽象的概念,在不同场景可以用不同方式实现。
比如 jQuery 里的表驱动 hook 实现,$.type
方法:
(function(window, undefined) {
var // 用于预存储一张类型表用于 hook
class2type = {};
// 原生的 typeof 方法并不能区分出一个变量它是 Array 、RegExp 等 object 类型,jQuery 为了扩展 typeof 的表达力,因此有了 $.type 方法
// 针对一些特殊的对象(例如 null,Array,RegExp)也进行精准的类型判断
// 运用了钩子机制,判断类型前,将常见类型打表,先存于一个 Hash 表 class2type 里边
jQuery.each(
"Boolean Number String Function Array Date RegExp Object Error".split(" "),
function(i, name) {
class2type["[object " + name + "]"] = name.toLowerCase();
}
);
jQuery.extend({
// 确定 JavaScript 对象的类型
// 这个方法的关键之处在于 class2type[core_toString.call(obj)]
// 可以使得 typeof obj 为 "object" 类型的得到更进一步的精确判断
type: function(obj) {
if (obj == null) {
return String(obj);
}
// 利用事先存好的 hash 表 class2type 作精准判断
// 这里因为 hook 的存在,省去了大量的 else if 判断
return typeof obj === "object" || typeof obj === "function"
? class2type[core_toString.call(obj)] || "object"
: typeof obj;
},
});
})(window);
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
这里的 hook 只是 jQuery 大量使用钩子的冰山一角,在对 DOM 元素的操作一块,attr 、val 、prop 、css 方法大量运用了钩子,用于兼容 IE 系列下的一些怪异行为。
在遇到钩子函数的时候,要结合具体情境具体分析,这些钩子相对于表驱动而言更加复杂,它们的结构大体如下,只要记住钩子的核心原则,保持代码整体逻辑的流畅性,在特殊的情境下去处理一些特殊的情况:
var someHook = {
get: function(elem) {
// obtain and return a value
return "something";
},
set: function(elem, value) {
// do something with value
},
};
2
3
4
5
6
7
8
9
从某种程度上讲,钩子是一系列被设计为以你自己的代码来处理自定义值的回调函数。有了钩子,你可以将差不多任何东西保持在可控范围内。
从设计模式的角度而言,这种钩子运用了策略模式。
策略模式
将不变的部分和变化的部分隔开是每个设计模式的主题,而策略模式则是将算法的使用与算法的实现分离开来的典型代表。使用策略模式重构代码,可以消除程序中大片的条件分支语句。在实际开发中,我们通常会把算法的含义扩散开来,使策略模式也可以用来封装一系列的“业务规则”。只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以使用策略模式来封装它们。
策略模式的优点:
策略模式利用组合,委托和多态等技术思想,可以有效的避免多重条件选择语句;
策略模式提供了对开放-封闭原则的完美支持,将算法封装在独立的函数中,使得它们易于切换,易于理解,易于扩展。
策略模式中的算法也可以复用在系统的其它地方,从而避免许多重复的复制粘贴工作。
# 连贯接口
jQuery 的流行很大程度归功于它的链式调用,接口的连贯性及易记性。很多人将连贯接口看成链式调用,这并不全面,jQuery 的接口连贯性主要体现在以下几点。
# 链式调用
链式调用的主要思想就是使代码尽可能流畅易读,从而可以更快地被理解。有了链式调用,我们可以将代码组织为类似语句的片段,增强可读性的同时减少干扰。
// 传统写法
var elem = document.getElementById("foobar");
elem.style.background = "red";
elem.style.color = "green";
elem.addEventListener(
"click",
function(event) {
alert("hello world!");
},
true
);
// jQuery 写法
$("xxx")
.css("background", "red")
.css("color", "green")
.on("click", function(event) {
alert("hello world");
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 命令查询同体
也就是函数重载。
正常而言,应该是命令查询分离(Command and Query Separation,CQS),是源于命令式编程的一个概念。那些改变对象的状态(内部的值)的函数称为命令,而那些检索值的函数称为查询。原则上,查询函数返回数据,命令函数返回状态,各司其职。
而 jQuery 将 getter 和 setter 方法压缩到单一方法中创建了一个连贯的接口,使得代码暴露更少的方法,但却以更少的代码实现同样的目标。
# 参数映射及处理
jQuery 的接口连贯性还体现在了对参数的兼容处理上,方法如何接收数据比让它们具有可链性更为重要。
虽然方法的链式调用是非常普遍的,你可以很容易地在你的代码中实现,但是处理参数却不同,使用者可能传入各种奇怪的参数类型,而 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,
});
2
3
4
5
6
7
8
9
10
11
12
13
14
jQuery 的 on() 方法可以注册事件处理器。和 CSS() 一样它也可以接收一组映射格式的事件,但更进一步地,它允许单一处理器可以被多个事件注册:
// binding events by passing a map
jQuery("#some-selector").on({
click: myClickHandler,
keyup: myKeyupHandler,
change: myChangeHandler,
});
// binding a handler to multiple events:
jQuery("#some-selector").on("click keyup change", myEventHandler);
2
3
4
5
6
7
8
9
# jQuery 中的 setTimeout
熟悉 jQuery 的人都知道 DOM Ready 事件,Javascript 原生的 window.onload 事件是在页面所有的资源都加载完毕后触发的。如果页面上有大图片等资源响应缓慢, 会导致 window.onload 事件迟迟无法触发,所以出现了 DOM Ready 事件。
DOM Ready 事件在 DOM 文档结构准备完毕后触发,即在资源加载前触发。另外我们需要在 DOM 准备完毕后,再修改 DOM 结构,比如添加 DOM 元素等。
而为了完美实现 DOM Ready 事件,兼容各浏览器及低版本 IE(针对高级的浏览器,可以使用 DOMContentLoaded 事件,省时省力),在 jQuery.ready() 方法里,运用了 setTimeout() 方法的一个特性,在 setTimeout 中触发的函数, 一定是在 DOM 准备完毕后触发。
jQuery.extend({
ready: function(wait) {
// 如果需要等待,holdReady() 的时候,把 hold 住的次数减1,如果还没到达0,说明还需要继续 hold 住,return 掉
// 如果不需要等待,判断是否已经 ready 过了,如果已经 ready 过了,就不需要处理了。异步队列里边的 done 的回调都会执行了
if (wait === true ? --jQuery.readyWait : jQuery.isReady) {
return;
}
// 确定 body 存在
if (!document.body) {
// 如果 body 还不存在,DOMContentLoaded 未完成,此时
// 将 jQuery.ready 放入定时器 setTimeout 中
// 不带时间参数的 setTimeout(a) 相当于 setTimeout(a,0)
// 但是这里并不是立即触发 jQuery.ready
// 由于 javascript 的单线程的异步模式
// setTimeout(jQuery.ready) 会等到重绘完成才执行代码,也就是 DOMContentLoaded 之后才执行 jQuery.ready
// 所以这里有个小技巧:在 setTimeout 中触发的函数, 一定会在 DOM 准备完毕后触发
return setTimeout(jQuery.ready);
}
// Remember that the DOM is ready
// 记录 DOM ready 已经完成
jQuery.isReady = true;
// If a normal DOM Ready event fired, decrement, and wait if need be
// wait 为 false 表示ready事情未触发过,否则 return
if (wait !== true && --jQuery.readyWait > 0) {
return;
}
// If there are functions bound, to execute
// 调用异步队列,然后派发成功事件出去(最后使用 done 接收,把上下文切换成 document,默认第一个参数是 jQuery。
readyList.resolveWith(document, [jQuery]);
// Trigger any bound ready events
// 最后 jQuery 还可以触发自己的 ready 事件
// 例如:
// $(document).on('ready', fn2);
// $(document).ready(fn1);
// 这里的 fn1会 先执行,自己的 ready 事件绑定的 fn2 回调后执行
if (jQuery.fn.trigger) {
jQuery(document)
.trigger("ready")
.off("ready");
}
},
});
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