jQuery 源码梳理

jQuery 源码梳理

因为最近在项目当中使用 jQuery 比较多,所以打算抽点时间深入学习一下 jQuery 源码的相关内容,也算是学习笔记记录吧,主要参考的是 jQuery 技术内幕 这本书籍,下面我们就先从总体架构部分开始看起

源码的总体架构

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
 16  (function (window, undefined) {
// 构造 jQuery 对象
22 var jQuery = (function() {
25 var jQuery = function(selector, context) {
27 return new jQuery.fn.init(selector, context, rootjQuery)
28 },

// 一堆局部变量声明
97 jQuery.fn = jQuery.prototype = {
98 constructor: jQuery,
99 init: function(selector, context, rootjQuery) { ... },
// 一堆原型属性和方法
319 };

322 jQuery.fn.init.prototype = jQuery.fn;

324 jQuery.extend = jQuery.fn.extend = function() { ... }
388 jQuery.extend({
// 一堆静态属性和方法
892 });

955 return jQuery

957 })();

// 省略其他模块的代码,主要是功能性代码,列表如下

// 工具函数 Utilities
// 异步队列 Deferred
// 浏览器测试 Support
// 数据缓存 Data
// 队列 queue
// 属性操作 Attribute
// 事件处理 Event
// 选择器 Sizzle
// DOM 遍历
// DOM 操作
// CSS 操作
// 异步请求 Ajax
// 动画 FX
// 坐标和大小

9246 window.jQuery = window.$ = jQuery

9266 })(window)

jQuery 的最外层是一个自调用匿名函数,通过定义一个匿名函数,创建了一个私有的命名空间,该命名空间的变量和方法,不会破坏全局的命名空间,参数中传入 window 变量,使得 window 由全局变量变为局部变量,当在 jQuery 代码块中访问 window 时,不需要将作用域链回退到顶层作用域,这样可以更快的访问 window

更重要的是将 window 作为参数传入,可以在压缩代码时进行优化,传入 undefined 是因为在自调用匿名函数的作用域内,确保 undefined 是真的未定义,因为 undefined 在某些浏览器下是能够被重写或是被赋予新的值

下面我们就先来看看它最为重要的一个方法,也就是构造函数 jQuery(),它一共有七种用法,如下

jQuery(selector, context)

接收一个 CSS 选择器表达式(selector)和可选的选择器上下文(Context),返回一个包含了匹配的 DOM 元素的 jQuery 对象,例如,在一个事件监听函数中,可以像下面这样限制查找范围

1
2
3
4
$('div.foo').click(function() {
// 限定查找范围
$('span', this).addClass('bar')
});
  • 如果选择器表达式 selector 是简单的 "#id" ,且没有指定上下文 Context,则调用浏览器原生方法 document.getElementById() 查找属性 id 等于指定值的元素
  • 如果是比 "#id" 复杂的选择器表达式或指定了上下文,则通过 jQuery 方法 .find() 查找,因此 $('span', this) 等价于 $(this).find('span')

jQuery(html, ownerDocument) 和 jQuery(html, props)

用所提供的 HTML 代码创建 DOM 元素

1
2
3
4
5
6
7
8
/*   
* 两种方式都可以往 body 中插入 div
* 1,$('<div>').appendTo('body')
* 2,$('<div></div>').appendTo('body')
*/

// 多标签嵌套
$('<div><span>foo</span></div>').appendTo('body')

jQuery(element or elementsArray)

如果传入一个 DOM 元素或 DOM 元素数组,则把 DOM 元素封装到 jQuery 对象中并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 传入 DOM 元素
$('li').each(function (index, ele) {
$(ele).on('click', function () {
// 这里的 DOM 元素就是 this
$(this).css('background', 'red')
})
})

// 传入 DOM 数组
var aLi = document.getElementsByTagName('li')

// 集合转数组
aLi = [].slice.call(aLi)

var $aLi = $(aLi)

// 所有的 li 的内容都变成 `我是 jQuery 对象`
$aLi.html(`我是 jQuery 对象`)

jQuery(object)

如果传入一个普通 JavaScript 对象,则把该对象封装到 jQuery 对象中并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个普通 JavaScript 对象
var foo = { foo: 'bar', hello: 'world' }

// 封装成 jQuery 对象
var $foo = $(foo)

// 绑定一个事件
$foo.on('custom', function () {
console.log(`custom event was called`)
});

// 触发这个事件
$foo.trigger('custom') // 在控制台打印 `custom event was called`

jQuery(callback)

当传进去的参数是函数的时候,则在 document 对象上绑定一个 ready 事件监听函数,当 DOM 结构加载完成的时候执行

1
2
3
4
5
6
$(function() { })

// 以上代码和下面的效果是一样的
$(document).ready(function () {
// ...
})

jQuery(jQuery object)

如果传入一个 jQuery 对象,则创建该 jQuery 对象的一个副本并返回,副本与传入的 jQuery 对象引用完全相同的 DOM 元素

jQuery()

如果不传入任何的参数,则返回一个空的 jQuery 对象,属性 length0,这个功能可以用来复用 jQuery 对象,例如,创建一个空的 jQuery 对象,然后在需要时先手动修改其中的元素,再调用 jQuery 方法,从而避免重复创建 jQuery 对象

jQuery.fn.init()

在了解完 jQuery 的总体架构以后,我们下面再来深入的了解一下初始化方法 jQuery.fn.init() 的构成,具体源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 16  (function (window, undefined) {
// 构造 jQuery 对象
22 var jQuery = (function() {
25 var jQuery = function(selector, context) {
27 return new jQuery.fn.init(selector, context, rootjQuery)
28 },

// 省略

// 这一步操作我们下面会进行介绍,所以写在这里
322 jQuery.fn.init.prototype = jQuery.fn

955 return jQuery

957 })()

9266 })(window)

我们可以发现,这里有一个 return new jQuery.fn.init() 操作,那么为什么要这样操作呢?其实在 JavaScript 当中我们知道,如果构造函数有返回值,运算符 new 所创建的对象会被丢弃,返回值将作为 new 表达式的值,所以 jQuery 通过在构造函数 jQuery() 内部用运算符 new 创建并返回另一个构造函数的实例,省去了构造函数 jQuery() 前面的运算符 new,即创建 jQuery 对象时,可以省略运算符 new 直接写 jQuery() 或者 $()

下面我们就来具体看看 jQuery.fn.init() 方法的构成

jQuery.fn.init() 和 jQuery.fn.init.prototype = jQuery.fn

源码如下

1
2
3
4
5
6
7
8
9
10
11
(function (window, undefined) {

// ...

jQuery.fn.init.prototype = jQuery.fn

// 省略其他模块的代码

window.jQuery = window.$ = jQuery

})(window)

当我们在调用 jQuery 构造函数时,实际返回的是 jQuery.fn.init() 的实例,在执行 jQuery.fn.init.prototype = jQuery.fn 时,用构造函数 jQuery() 的原型对象覆盖了构造函数 jQuery.fn.init() 的原型对象,从而使构造函数 jQuery.fn.init() 的实例也可以访问构造函数 jQuery() 的原型方法和属性,那么这里就存在一个问题了

为什么要覆盖构造函数 jQuery() 的原型对象 jQuery.prototype

因为在原型对象 jQuery.prototype 上定义的属性和方法会被所有 jQuery 对象继承,这样可以有效减少每个 jQuery 对象所需的内存,下面我们就正式来看一下 jQuery.fn.init() 这个方法

jQuery.fn.init(selector, context, rootjQuery)

构造函数 jQuery.fn.init() 负责解析参数 selectorContext 的类型,并执行相应的逻辑,最后返回 jQuery.fn.init() 的实例

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
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// (1)定义构造函数 jQuery.fn.init(selector, context, rootjQuery) 它接受 3 个参数
// 参数 selector,可以是任意类型的值,但只有 undefined,DOM 元素,字符串,函数,jQuery 对象,普通 JavaScript 对象这几种类型是有效的
// 参数 context,可以不传入,或者传入 DOM 元素,jQuery 对象,普通 JavaScript 对象之一
// 参数 rootjQuery,包含了 document 的 jQuery 对象,用于 document.getElementById() 查找失败,selector 是选择器表达式且未指定 context,selector 是函数的情况
init: function(selector, context, rootjQuery) {
var match, elem, ret, doc

// (2)参数 selector 可以转换为 false,例如是 undefined,空字符串,null 等则直接返回 this,此时 this 是空 jQuery 对象,其属性 length 等于 0
// 如果 selector 为空,比如 $(''), $(null), or $(undefined),则返回空的 jQuery 对象
if (!selector) {
// 此时 this 为空 jQuery 对象
return this
}

// (3)如果参数 selector 有属性 nodeType,则认为 selector 是 DOM 元素,比如 $(DOMElement)
if (selector.nodeType) {
// 将第一个元素和属性 context 指向 selector
this.context = this[0] = selector
this.length = 1
return this
}

// (4)如果参数 selector 是字符串 'body',手动设置属性 context 指向 document 对象,第一个元素指向 body 元素,最后返回包含了 body 元素引用的 jQuery 对象
// 如果选中的是 body,则利用 !context 进行优化(因为 body 只会出现一次)
if (selector === 'body' && !context && document.body) {
// context 指向 document 对象
this.context = document
this[0] = document.body
this.selector = selector
this.length = 1
return this
}

// (5)如果参数 selector 是其他字符串,则先检测 selector 是 HTML 代码还是类似 #id 这样的选择符
if (typeof selector === 'string') {
// 如果是以 '<' 开头 以 '>' 结尾,且长度大于等于 3
if (selector.charAt(0) === "<" && selector.charAt(selector.length - 1) === ">" && selector.length >= 3) {
// 则跳过 queckExpr 正则检查,这里假设为 HTML 片段,比如 '<div></p>'
match = [null, selector, null]
} else {
// 否则,用正则 quickExpr 检测参数 selector 是否是稍微复杂
// 一些的 HTML 代码(如'abc<div>' )或 #id,匹配结果存放在数组 match 中
match = quickExpr.exec(selector)
}

// (6)如果参数 selector 是单独标签
// 根据上面正则返回的结果,调用 document.createElement() 创建标签对应的 DOM 元素
// 如果 match[1] 不是 undefined,即参数 selector 是 HTML 代码
// 或者 match[2] 不是 undefined,即参数 selector 是 #id,并且未传入参数 context
// 所以就省略了对 match[2] 的判断,完整的表达式为 if (match && (match[1] || match[2] && !context))
if (match && (match[1] || !context)) {

// HANDLE: $(html) -> $(array)
// 开始处理参数 selector 是 HTML 代码的情况
if (match[1]) {

// 先修正 context 和 doc
context = context instanceof jQuery ? context[0] : context
doc = (context ? context.ownerDocument || context : document)

// 正则 rsingleTag 检测 HTML 代码是否是单独标签,匹配结果存放在数组 ret 中
// rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/
ret = rsingleTag.exec(selector)

// 如果数组 ret 不是 null,则是单独标签,调用 document.createElement() 创建标签对应的 DOM 元素
if (ret) {

// 如果 context 是普通对象,则调用 jQuery 方法 .attr() 并传入参数 context
// 同时把参数 context 中的属性、事件设置到新创建的 DOM 元素上
if (jQuery.isPlainObject(context)) {
// 之所以放在数组中,是方便后面的 jQuery.merge() 方法调用
selector = [document.createElement(ret[1])]
// 调用 attr 方法,传入参数 context
jQuery.fn.attr.call(selector, context, true)

} else {
selector = [doc.createElement(ret[1])]
}

// (7)如果参数 selector 是复杂 HTML 代码,则利用浏览器的 innerHTML 机制创建 DOM 元素
} else {
ret = jQuery.buildFragment([match[1]], [doc])
selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes
}

return jQuery.merge(this, selector)

// (8)参数 selector 是 '#id',且未指定参数 context
} else {
elem = document.getElementById(match[2])

if (elem && elem.parentNode) {
// 即使是 documen.getElementById 这样核心的方法也要考虑到浏览器兼容问题,可能找到的是 name 而不是 id
if (elem.id !== match[2]) {
return rootjQuery.find(selector)
}

// 如果所找到元素的属性 id 值与传入的值相等,则设置第一个元素
// 属性 length、context、selector,并返回当前 jQuery 对象
this.length = 1
this[0] = elem
}

this.context = document
this.selector = selector
return this
}

// HANDLE: $(expr, $())
// (9)参数 selector 是选择器表达式
// 没有指定上下文,执行 rootjQuery.find(),指定了上下文且上下文是 jQuery 对象,执行 context.find()
} else if (!context || context.jquery) {
return (context || rootjQuery).find(selector)

// HANDLE: $(expr, context)
// (which is just equivalent to: $(context).find(expr)

// 如果指定了上下文,且上下文不是 jQuery 对象
} else {
// 先创建一个包含 context 的 jQuery 对象,然后调用 find 方法
return this.constructor(context).find(selector)
}

// HANDLE: $(function)
// (10)参数 selector 是函数
} else if (jQuery.isFunction(selector)) {
return rootjQuery.ready(selector)
}

// selector 是 jquery 对象
// 如果参数 selector 含有属性 selector,则认为它是 jQuery 对象,将会复制它的属性 selector 和 context
if (selector.selector !== undefined) {
this.selector = selector.selector
this.context = selector.context
}

// 参数 selector 是任意其他值,最后(合并)返回当前 jQuery 对象
return jQuery.makeArray(selector, this)
},

辅助方法

关于架构方面的内容我们暂时就介绍这么多,下面我们再来看看 jQuery 当中的一些辅助方法,而这些辅助方法的实现方式也是我们在平时开发过程中可以去借鉴使用的,下面我们就先来看一些在源码当中经常可以看到的辅助方法

  • jQuery.noConflict([removeAll])
  • jQuery.isFunction(obj)
  • jQuery.isArray(obj)
  • jQuery.type(obj)
  • jQuery.isWindow(obj)
  • jQuery.isNumeric(value)
  • jQuery.isPlainObject(object)
  • jQuery.makeArray(obj)
  • jQuery.inArray(value, array[, fromIndex])
  • jQuery.merge(first, second)
  • jQuery.grep(array, function(elementOfArray, indexInArray)[, invert])

jQuery.noConflict([removeAll])

方法 jQuery.noConflict([removeAll]) 用于释放 jQuery 对全局变量 $ 的控制权,可选参数 removeAll 表示是否释放对全局变量 jQuery 的控制权,$ 仅仅是 jQuery 的别名,没有 $ ,其余功能也是可以正常使用的(使用 jQuery),如果需要使用另一个 JavaScript 库,可以调用 $.noConflict() 返回 $ 给其他库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 先把可能存在的 window.jQuery 和 $ 备份到局部变量 _jQuery 和 _$ 上
_jQuery = window.jQuery,

_$ = window.$,

jQuery.extend({
noConflict: function (deep) {
// 只有当前 jQuery 库持有全局变量 $ 的情况下,才会释放 $ 的控制权给前一个 JavaScript 库
if (window.$ === jQuery) {
window.$ = _$
}

// 只有在当前 jQuery 库持有全局变量 jQuery 的情况下,才会释放 jQuery 的控制权给前一个 JavaScript 库
if (deep && window.jQuery === jQuery) {
window.jQuery = _jQuery
}

return jQuery
}
}),

// ...略

window.jQuery = window.$ = jQuery

如果有必要(例如,在一个页面中使用多个版本的 jQuery 库,但很少有这样的必要),也可以释放全局变量 jQuery 的控制权,只需要给这个方法传入参数 true 即可,从 jQuery 1.6 开始增加了对 window.$ === jQuery 的检测,如果不检测,则每次调用 jQuery.noConflict() 时都会释放 $ 给前一个 JavaScript 库,不过建议页面当中还是只保持一个对于 $ 的引用,因为当页面中有两个以上定义了 $JavaScript 库时,对 $ 的管理将会变得混乱

jQuery.isFunction(obj), jQuery.isArray(obj)

这两个方法主要用于判断传入的参数是否是函数(数组),这两个方法的实现依赖方法 jQuery.type(obj),通过返回值是否是 function 或者 Array 来进行判断

1
2
3
4
5
6
7
isFunction: function(obj) {
return jQuery.type(obj) === 'function'
},

isArray: Array.isArray || function (obj) {
return jQuery.type(obj) === 'array'
},

jQuery.type(obj)

这个方法主要用于判断参数的 JavaScript 类型,在平常的开发当中也是经常会遇到的,如果参数是 undefinednull,返回 'undefined''null'(注意是字符串类型),如果参数是内部对象,则返回对应的字符串名称,其他一律返回 Object

1
2
3
4
5
6
7
8
9
10
11
12
13
type: function (obj) {

// 若为 undefined/null ==> 转换为字符串 'undefined'/'null'
return obj == null ? String(obj)

// 以上的返回值形式为 [object class],其中 class 是内部对象类
// 例如 Object.prototype.toString.call(true) 会返回 [object Boolean]
// 然后从对象 class2type 中取出 [object class] 对应的小写字符串并返回
: class2type[toString.call(obj)]

// 如果未取到则一律返回 object
|| 'object'
},

下面是原型方法 toString()class2type 的定义及初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
toString = Object.prototype.toString,

// [[Class]] ==> type pairs
class2type = {}

// class2type 的定义
jQuery.each('Boolean Number String Function Array Date RegExp Object').split(' '), function (i, name) {
class2type['[object' + name + ']'] = name.toLowerCase()
}

// 对象 class2type 初始化后的结构为
{
'[object Array]': 'array'
'[object Boolean]': 'boolean'
'[object Date]': 'date'
'[object Function]': 'function'
'[object Number]': 'number'
'[object Object]': 'object'
'[object RegExp]': 'regexp'
'[object String]': 'string'
}

jQuery.isWindow(obj)

这个方法主要用于用于判断传入的参数是否是 window 对象,通过检测是否存在特征属性 setInterval 来实现

1
2
3
4
5
6
7
8
9
// 1.7.2 之前
isWindow: function (obj) {
return obj && typeof obj === 'object' && 'setInterval' in obj
},

// 1.7.2 之后,该方法修改为检测特征属性 window, 该属性是对窗口自身的引用
isWindow: function (obj) {
return obj != null && obj == obj.window
}

jQuery.isNumeric(value)

这个方法主要用于用于判断传入的参数是否是数字,或者看起来是否像数字

1
2
3
isNumeric: function (obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj)
},

先用 parseFloat(obj) 尝试把参数解析为数字,然后判断其是否合法,然后在使用 isFinite(obj) 判断其是否是有限的,均通过验证则返回 true

jQuery.isPlainObject(object)

用于判断传入的参数是否为纯粹的对象,即 {}new Object() 创建的对象(使用 Object.create(null) 创建的空对象也是属于纯粹的对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
isPlainObject: function (obj) {
if (!obj || jQuery.type(obj) !== 'object' || obj.nodeType || jQuery.isWindow(obj)) {
return false
}

try {
if (obj.constructor
&& !hasOwn.call(obj, 'constructor')
&& !hasOwn.call(obj.constructor.property, 'isPrototypeOf')) {
return false
}
} catch (e) {
return false
}

var key
for (key in obj) { }

return key === undefined || hasOwn.call(obj, key)

},

如果参数 obj 满足下列条件之一,则返回 false

  • 参数 obj 可以转换为 false
  • Object.prototype.toString.call(obj) 返回的不是 [object, Object]
  • 参数 objDOM 元素
  • 参数 objwindow 对象

如果不满足以上所有条件,则至少可以确定参数 obj 是对象,然后使用 try-catch 来检查对象 obj 是否由构造函数 Object() 创建,如果对象 obj 满足以下所有条件,则认为不是由构造函数 Object() 创建,而是由自定义构造函数创建,返回 false

  • 对象 obj 含有属性 constructor,由构造函数创建的对象都有一个 constructor 属性,默认引用了该对象的构造函数,如果对象 obj 没有属性 constructor,则说明该对象必然是通过对象字面量 {} 创建的
  • 对象 obj 的属性 constructor 是非继承属性,默认情况下,属性 constructor 继承自构造函数的原型对象,如果属性 constructor 是非继承属性,说明该属性已经在自定义构造函数中被覆盖
  • 对象 obj 的原型对象中没有属性 isPrototypeOf,属性 isPrototypeOfObject 原型对象的特有属性,如果对象 obj 的原型对象中没有,说明不是由构造函数 Object() 创建,而是由自定义构造函数创建
  • 执行以上检测时抛出了异常,在 IE 8/9 中,在某些浏览器对象上执行以上检测时会抛出异常,也应该返回 false

函数 hasOwn() 指向 Object.prototype.hasOwnProperty(property),用于检查对象是否含有执行名称的非继承属性,而最后的 for-in 则是检查对象 obj 的属性是否都是非继承属性,如果没有属性,或者所有属性都是非继承属性,则返回 true,如果含有继承属性,则返回 false,执行 for-in 循环时,JavaScript 会先枚举非继承属性,再枚举从原型对象继承的属性

最后,如果对象 obj 的最后一个属性是非继承属性,则认为所有属性都是非继承属性,返回 true,如果最后一个属性是继承属性,即含有继承属性,则返回 false

jQuery.makeArray(obj)

可以将一个类数组对象转换为真正的数组(类似于方法 Array.from()),在 jQuery 内部,还可以为方法 jQuery.makeArray() 传入第二个参数,这样一来,第一个参数中的元素被合并入第二个参数,最后会返回第二个参数,此时返回值的类型不一定是真正的数组

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
push = Array.prototype.push,

// 定义方法 makeArray() 接收两个参数
// array 待转换的对象,可以是任何类型
// results 仅在 jQuery 内部使用,如果传入参数 results,则在该参数上添加元素
makeArray: function (array, results) {

// 定义返回值,如果传入了参数 results 则把该参数作为返回值,否则新建一个空数组返回
var ret = results || []

// 过滤掉 null undefined
if (array != null) {

var type = jQuery.type(array)

if (array.length == null
|| type === 'string'
|| type === 'function'
|| type === 'regexp'
|| jQuery.isWindow(array)) {

// 之所以不是 ret.push(array) 是因为 ret 不一定是真正的数组,如果只传入 array,则返回值
// ret 是真正的数组,如果还传入了第二个参数,则返回值 ret 取决于该参数的类型
push.call(ret, array)

} else {

// 否则认为 array 是数组或类数组对象,执行合并
jQuery.merge(ret, array)
}
}

// 返回
return ret
}

jQuery.inArray(value, array[, fromIndex])

在数组中查找指定的元素并返回其下标,未找到则返回 -1

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
// 定义方法 inArray 接收三个参数
// elem 需要查找的值
// array 数组,将遍历这个数组来查找参数 value 在其中的下标
// i 指定开始的位置,默认是 0(即查找整个数组)
inArray: function (elem, array, i) {

var len

// 过滤掉可以转换为 false 的情况
if (array) {

// 如果支持 indexOf 这调用 indexOf 返回下标
if (indexOf) {
return indexOf.call(array, elem, i)
}

len = array.length

// 修正参数 i,如果未指定 i,则初始化为 0,表示从头开始
// 如果 i < 0,则加上数组长度 len,即从末尾开始计算
// 调用 Math.max() 在 0 和 len + i 之间取最大值,如果 len + i 依然 < 0 ,则修正为 0 ,从头开始
i = i ? i < 0 ? Math.max(0, len + i) : i : 0

// 开始遍历,查找与指定值 elem 相等的元素,并返回其下标
for (; i < len; i++) {

// 如果 i in array 返回 false,则说明 array 的下标是不连续的,无需比较
if (i in array && array[i] === elem) {
return i
}
}
}

return -1
}

通常我们会比较 jQuery.inArray() 的返回值是否大于 0 来判断某个元素是否是数组张的元素

1
2
3
if (jQuery.inArray(elem, array) > 0) {
// elem 是 array 中的元素
}

但是这种写法比较繁琐,可以利用按位非运算符(~)简化上面的代码

1
2
3
if (~jQuery.inArray(elem, aray)) {
// elem 是 array 中的元素
}

按位非运算符(~)会将运算数的所有位取反,相当于改变它的符号并且减 1

1
2
3
4
~-1 == 0  // true
~0 == -1 // true
~1 == -2 // true
~2 == -3 // true

更进一步,可以结合使用按位非运算符(~)和逻辑非运算符(!)把 jQuery.inArray() 的返回值转换为布尔类型

1
2
3
// 如果 elem 可以匹配 array 中的某个元素,则该表达式的值为 true
// 如果 elem 匹配不到 array 中的某个元素,则该表达式的值为 false
!!~jQuery.inArray(elem, array)

jQuery.merge(first, second)

方法 jQuery.merge() 用于合并两个数组的元素到第一个数组中,事实上,第一个参数可以是数组或类数组对象,即必须含有整型(或可以转换为整型)属性 length,第二个参数则可以是数组,类数组对象或任何含有连续整型的对象,合并行为是不可逆的,即将第二个数组合并到第一个以后,第一个数组就改变了,如果不希望如此,则可以在调用 jQuery.merge() 之前创建一份数组的备份

1
var newArray = $.merge([], oldArray)

方法 jQuery.merge() 的定义如下

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
// 定义方法 jQuery.merge() 接收两个参数
// first 数组或类对象,必须含有整型(或可以转换为整型)属性 length
// second 数组,类数组对象或任何含有连续整型的对象,合并至 first
merge: function (first, second) {
// first.length 必须是整型或者可以转换为整型,否则后面 i++ 返回 NaN
var i = first.length, j = 0

// 如果是数值类型,则当数组处理,添加至 first
if (typeof second.length === 'number') {
for (var l = second.length; j < l; j++) {
first[i++] = second[j]
}

// 如果没有 length,则当作含有连续整型属性的对象,例如 {0: 'a', 1: 'b'}
// 把其中的非 undefined 元素逐个插入参数 first 中
} else {
while (second[j] !== undefined) {
first[i++] = second[j++]
}
}

// 修正 length 因为 first 可能不是真正的数组
first.length = i

// 返回参数
return first
}

jQuery.grep(array, function(elementOfArray, indexInArray)[, invert])

用于查找数组当中满足过滤函数的元素,原数组不会受影响,如果参数 invert 没有传入或者为 false 元素只有在过滤函数返回 true,或者返回值可以转换为 true 的时候,才会被保存在最终的结果数组中,即返回一个满足回调函数的元素数组,如果参数 inverttrue,则反之

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义方法 jQuery.grep() 接收三个参数
// array 待遍历查找的数组
// callback 过滤每个元素的函数,执行的时候传入两个参数,当前元素和它的下标,返回一个布尔值
// inv 如果参数 inv 是 false 或者没有传入,jQuery.grep() 会返回一个满足回调函数的元素数组,如果为 true,则返回一个不满足回调函数的元素数组
grep: function (elems, callback, inv) {
var ret = [], retVal, inv = !!inv
for (var i = 0; length = elems.length, i < length; i++) {
retVal = !!callback(elems[i], i)
if (inv !== retVal) {
ret.push(elems[i])
}
}
return ret
}

其他方法

我们在上面介绍了源码当中的辅助函数,下面我们再来看看构造 jQuery 对象模块的原型属性和方法,因为它们当中某些方法都是依赖于其他的方法来进行实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
jQuery.fn = jQuery.prototype            // 原型属性和方法 
.constructor // 指向构造函数 jQuery()
.init(selector, context, rootjQuery) // 构造函数,解析参数 selector 和 context 的类型,并执行相应的逻辑,最后返回 jQuery.fn.init() 的实例
.selector // 记录 jQuery 査找和过滤 DOM 元素时的选择器表达式
.jquery // 正在使用的 jQuery 版本号
.length // jQuery 对象中元素的个数
.size() // 返回当前 jQuery 对象中元素的个数
.toArray() // 将当前 jQuery 对象转换为真正的数组
.get([index]) // 返回当前 jQuery 对象中指定位置的元素或包含了全部元素的数组
.pushStack(elements, name, arguments) // 创建一个新的空 jQuery 对象,然后把 DOM 元素集合放入这个jQuery 对象中,并保留对当前 jQuery 对象的引用
.each(function(index, Element)) // 遍历当前 jQuery 对象中的元素,并在每个元素上执行回调函数
.ready(handler) // 绑定 ready 事件
.eq(index) // 将匹配元素集合缩减为位于指定位置的新元素
.first() // 将匹配元素集合缩减为集合中的第一个元素
.last() // 将匹配元素集合缩减为集合中的最后一个元素
.slice() // 将匹配元素集合缩减为指定范围的子集
.map(callback(index, domElement)) // 遍历当前 jQuery 对象中的元素,并在每个元素上执行回调函数, 将回调函数的返回值放入一个新的 jQuery 对象中
.end() // 结束当前链条中最近的筛选操作,并将匹配元素集合还原为之前的状态
.push() // Array.prototype.push
.sort() // [].sort
.splice() // [].splice

createSafeFragment(document)

该方法的主要作用是用于处理兼容问题

1
2
3
4
5
6
7
8
9
10
11
12
13
function createSafeFragment(document) {
var list = nodeNames.split('|')
safeFrag = document.createDocumentFragment()
if (safeFrag.createElement) {
while (list.length) {
safeFrag.createElement(list.pop())
}
}
return safeFrag
}

var nodeNames = 'abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|' +
'header|hgroup|mark|meter|nav|output|progress|section|summary|time|video'

变量 nodeNames 中存放了所有的 html5 标签,createSafeFragment() 在传入的文档对象 document 上创建一个新的文档片段,然后在该文档片段上逐个创建 html5 元素,从而兼容不支持 html5 的浏览器,使之正确的解析和渲染

fixDefaultChecked(elem)

主要用于修正复选框和单选按钮的选中状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Used in clean, fixes the defaultChecked property
function fixDefaultChecked(elem) {
if (elem.type === 'checkbox' || elem.type === 'radio') {
elem.defaultChecked = elem.checked
}
}

// Finds all inputs and passes them to fixDefaultChecked
function findInputs(elem) {
var nodeName = (elem.nodeName || '').toLowerCase()
if (nodeName === 'input') {
fixDefaultChecked(elem)
// Skip scripts, get other children
} else if (nodeName !== 'script' && typeof elem.getElementsByTagName !== 'undefined') {
jQuery.grep(elem.getElementsByTagName('input'), fixDefaultChecked)
}
}

遍历转换后的 DOM 元素集合,在每个元素上调用函数 findInputs(elem),函数 findInputs(elem) 会找出其中的复选框和单选按钮,并调用函数 fixDefaultChecked(elem) 把属性 checked 的值赋值给属性 defaultChecked

.pushStack(elements, name, arguments)

原型方法 .pushStack() 创建一个新的空 jQuery 对象,然后把 DOM 元素集合放进这个 jQuery 对象中,并保留对当前 jQuery 对象的引用,它对 jQuery 对象遍历,DOM 查找,DOM 遍历,DOM 插入等方法提供支持

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
// 定义方法 .pushStack() 接收三个参数(即构建一个新的 jQuery 对象并入栈,新对象位于栈顶)
// elems 将放入新 jQuery 对象的元素数组(或类数组)
// name 产生元素数组 elems 的 jQuery 方法名
// selector 传给 jQuery 方法的参数,用于修正原型属性 .selector
pushStack: function (elems, name, selector) {

// 创建一个空的 jQuery 对象,this.constructor 指向构造函数 jQuery
var ret = this.constructor()

// 合并参数 elems
if (jQuery.isArray(elems)) {
// 如果是数组
push.apply(ret, elems)
} else {
// 不是数组的情况
jQuery.merge(ret, elems)
}

// 设置属性 prevObject, 指向当前 jQuery 对象,从而形成一个链式栈
ret.prevObject = this

// 指向当前 jQuery 的上下文
ret.context = this.context

// 在 ret 上设置属性 selector,方便调试
if (name === 'find') {
ret.selector = this.selector + (this.selector ? ' ' : '') + selector
} else if (name) {
ret.selector = this.selector + '.' + name + '(' + selector + ')'
}

// 返回 ret
return ret
}

.end()

结束当前链中最近的筛选操作,并将匹配元素集合还原为之前的状态

1
2
3
end: function() {
return this.prevObject || this.constructor(null)
}

返回一个 jQuery 对象,如果属性 prevObject 不存在,则构建一个空的 jQuery 对象返回,简单来说就是方法 pushStach() 用于入栈,而 end() 则用于出栈,比如

1
2
3
4
$('ul li').find('div').css('backgroundColor','red')
.end()
.find('span')css('backgroundColor','blue')
.end()

.eq(index),.first(),.last(),.slice(start[, end])

方法 .first().last() 通过调用 .eq(index) 实现,.eq(index) 主要通过 .slice(start[, end]) 来实现,而 .slice(start[, end]) 则是通过调用 .pushStack(elements, name, arguments) 实现,方法调用链为依次如下

.first()/last() ==> .eq(index) ==> .slice(start[, end]) ==> .pushStack(elements, name, arguments)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
eq: function(i) {
// 如果 i 是字符串,就通过 '+i' 转换为数值
i = +i
return i === -1 ? this.slice(i) : this.slice(i, i + 1)
},

first: function() {
return this.eq(0)
},

last: function() {
return this.eq(-1)
},

slice: function() {
// 先借用数组方法 slice() 从当前 jQuery 对象中获取指定范围的子集(数组)
// 再调用方法 .pushStack() 把子集转换为 jQuery 对象,同时通过其中的 prevObject 属性来保留对当前 jQuery 对象的引用
return this.pushStack(slice.apply(this, arguments), 'slice', slice.call(arguments).join(','))
}

.push(value, …), .sort([orderfunc]), .splice(start,deleteCount, value, …)

方法 .push(value, ...) 向当前 jQuery 对象的末尾添加新元素,并返回新长度

1
2
var foo = $(document)
foo.push(document.body) // 2

方法 .sort([orderfunc]) 对当前 jQuery 对象中的元素进行排序,可以传入一个比较函数来指定排序方式

1
2
3
4
5
6
7
8
var foo = $([33, 4, 1111, 222])

foo.sort() // [1111, 222, 33, 4]

foo.sort(function (a, b) {
return a - b
})
// [4, 33, 222, 1111]

方法 .splice(start, deleteCount, value, ...) 向当前 jQuery 对象中插入, 删除或替换元素,如果从当前 jQuery 对象中删除了元素,则返回含有被删除元素的数组

1
2
3
4
5
var foo = $('<div id="d1" /><div id="d2" /><div id="d3" />')
// [<div id="d1"></div>, <div id="d2"></div>, <div id="d3"></div>]

foo.splice(1, 2)
// [<div id="d2"></div>, <div id="d3"></div>]

以上三个方法仅在内部使用,都指向同名的数组方法,因此它们的参数,功能和返回值与数组方法完全一致

1
2
3
push: push,
sort: [].sort,
splice: [].splice

jQuery.each(object, callback, args)

关于 each() 方法有一个需要注意的地方,那就是要区分 $.each()$(selector).each() 两者之间的区别,$(selector).each() 一般用于 jQuery 对象的遍历,它会为每个匹配元素规定要运行的函数

1
2
3
$('ul li').each(function(){
alert($(this).text())
})

通过源码可知,each 方法实际上调用的就是 jQuery.each() 方法

1
2
3
4
5
6
7
// ...

each: function(callback, args) {
return jQuery.each(this, callback, args)
}

// ...

$.each() 使用的范围就很广了,可用于遍历任何的集合(无论是数组或对象),下面是几个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 参数 i 为遍历索引值,n 为当前的遍历对象
var arr = [{ name: 'zhangsan', email: 'zhangsan@gmail.com' }, { name: 'lisi', email: 'lisi@gmail.com' }]
$.each(arr, function (i, n) {
console.log(`索引: ${i} 对应值为:${n.name}`)
})

var arr1 = ['one', 'two', 'three', 'four', 'five']
$.each(arr1, function () {
console.log(this)
})

var arr2 = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
$.each(arr2, function (i, item) {
console.log(item[0]) // 1 4 7
})

var obj = { one: 1, two: 2, three: 3, four: 4, five: 5 }
$.each(obj, function (key, val) {
console.log(obj[key]) // 1 2 3 4 5
})

方法 each() 遍历当前 jQuery 对象,并在每个元素上执行回调函数,每当回调函数执行时,会传递当前循环次数作为参数,循环次数从 0 开始计数,更重要的是,回调函数是在当前元素为上下文的语境中触发的,即关键字 this 总是指向当前元素,在回调函数中返回 false 可以终止遍历

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
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
// jQuery.each 方法用于遍历一个数组或对象,并对当前遍历的元素进行处理  
// jQuery.each 方法可以为处理函数增加附带的参数(带参数与不带参数的回调使用方法不完全一致)

// 静态方法 jQuery.each() 是一个通用的遍历迭代方法,用于无缝地遍历对象和数组
// 对于数组和含有 length 属性的类数组对象(如函数参数对象 arguments),该方法通过下标遍历,从 0 到 length - 1
// 对于其他对象则通过属性名遍历(for-in)
// 在遍历过程中,如果回调函数返回 false,则终止遍历

// ------------------------------------------

// 总的来说就是:
// 1. 对于对象,通过 for-in 循环遍历属性名,对于数组或类数组对象,则通过 for 循环遍历下标
// 2. 如果传入了参数 args,使用 apply,执行回调函数时只传入一个参数 args
// 3. 如果未传入参数 args,使用 call,执行回调函数时传入两个参数:下标或属性名,对应的元素或属性值

// ------------------------------------------

// 关于参数 args:传给回调函数 callback 的参数数组,可选
// 如果没有传入参数 args,则执行回调函数时会传入两个参数(下标或属性名,对应的元素或属性值)
// 如果传入了参数 args,则只把该参数传给回调函数
each: function (object, callback, args) {

// 当需要遍历的是一个对象时, name 变量用于记录对象的属性名
var name,

// 当需要遍历的是一个数组时, i 变量用于记录循环的数组下标
i = 0,

// 遍历数组长度,当需要遍历的对象是一个数组时存储数组长度
// 如果需要遍历的是一个对象, 则 length === undefined
length = object.length,

// 变量 isObj 表示参数 object 是对象还是数组,以便决定遍历方式
// 如果 object.length 是 undefined 或 object 是函数,则认为 object 是对象,设置变量 isObj 为 true,将通过属性名遍历
// 否则认为是数组或类数组对象,设置变量 isObj 为 false,将通过下标遍历
isObj = length === undefined || jQuery.isFunction(object)

// 回调函数具有附加参数时, 执行第一个分支
// if(!!args) {
if (args) {

// 需要遍历的是一个对象
if (isObj) {

// 遍历对象属性, name 是对象的属性名,再函数顶部已声明
for (name in object) {

// 调用 callback 回调函数, 且回调函数的作用域表示为当前属性的值
if (callback.apply(object[name], args) === false) {

// 如果在 callback 回调函数中使用 return false 则不执行下一次循环
break
}
}

// 需要遍历的是一个数组
} else {

// 循环变量的自增在循环内部执行
for (; i < length;) {

// 调用 callback 函数, 与上面的 callback 调用一致
// 此处 callback 函数中的 this 指向当前数组元素
// 根据下标 i 依次执行
if (callback.apply(object[i++], args) === false) {
break
}
}
}

// 回调函数没有附加参数时,执行第二个分支
} else {

// 需要遍历的是一个对象
if (isObj) {

for (name in object) {

// 调用 callback 回调函数
// 在不带参数的对象遍历中, 作用域表示为当前属性的值
// 且回调函数包含两个参数, 第一个数当前属性名, 第二个是当前属性值
if (callback.call(object[name], name, object[name]) === false) {

// 作用同上
break
}
}

// 需要遍历的是一个数组
} else {
for (var value = object[0]; i < length && callback.call(value, i, value) !== false; value = object[++i]) {
}
}
}

// jQuery 并没有把以上两段很相似的代码合并,这是因为在合并后需要反复判断变量 isObj 的值,避免性能下降
// 返回 object ,方法 .each 调用 jQuery.each() 的时候,把当前的 jQuery 对象作为参数 object 传入
// 在这里返回该参数,以支持链式语法
return object
}

jQuery.extend([deep], target, object1[, objectN])

先来看看怎么使用

1
2
3
4
$.extend(target [, object1 ] [, objectN ])

// 可以添加参数来指示是否深度合并
$.extend([deep ], target, object1 [, objectN ])

需要注意的是,第一个参数不支持传递 false

取值 释义
deep 可选,布尔类型,指示是否深度合并对象,默认为 false,如果该值为 true,且多个对象的某个同名属性也都是对象,则该对象的属性也将进行合并
target Object 类型,目标对象,其他对象的成员属性将被附加到该对象上
object1 可选,Object 类型,第一个被合并的对象
objectN 可选,Object 类型,第 N 个被合并的对象

简单来说,该方法的作用是用一个或多个其他对象来扩展一个对象,返回扩展后的对象,如果不指定 target,则是给 jQuery 命名空间本身进行扩展(有利于为 jQuery 增加新方法),如果第一个参数设置为 true,则 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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
jQuery.extend = jQuery.fn.extend = function () {

// 定义局部变量,参数介绍如下
var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false

// options 指向某个源对象
// name 表示某个源对象的某个属性名
// src 表示目标对象的某个属性的原始值
// copy 表示某个源对象的某个属性的值
// copyIsArray 指示变量 copy 是否是数组
// clone 表示深度复制时原始值的修正值
// target 指向目标对象
// i 表示源对象的起始下标
// length 表示参数的个数,用于修正变量 target
// deep 指示是否执行深度复制,默认为 false

// 修正目标对象 target、源对象起始下标 i
// 如果第一个参数是布尔,则修正为第一个为 deep,第二个为 target,期望对象从第三个元素开始
// 若第一个不是布尔,则是期望第二个元素开始(i 初始为 1)
if (typeof target === "boolean") {
deep = target
target = arguments[1] || {}
i = 2
}

// 如果 target 不是对象,函数,统一替换为 {} (因为在基本类型上设置非原生属性是无效的)
if (typeof target !== "object" && !jQuery.isFunction(target)) {
target = {}
}

// 如果两者相等,表示期望的源对象没有传入,则把 jQuery 和 jQuery.fn 作为目标,并且把源对象开始下标减一
// 从而使得传入的对象被当作源对象,相等有两种情况
// 1. extend(object) 只传入了一个参数
// 2. extend(deep, object) 第一个参数为布尔
if (length === i) {
target = this
--i
}

// 逐个遍历源对象
for (; i < length; i++) {

// 遍历源对象的属性
if ((options = arguments[i]) !== null) {
for (name in options) {

// 覆盖目标对象的同名属性
src = target[name]
copy = options[name]

// 避免 src(原始值)与 copy(复制值)相等进入死循环
if (target === copy) {
continue
}

// 如果原始值 src 不是数组 ==> 修正为空数组
// 复制值是普通 javaScript 对象,如果原始值 src 不是普通 javaScript 对象 ==> 修正为 {},修正后的 src 赋值给原始值的副本 clone

// 调用 jQuery.isPlainObject(copy) 判断复制值 copy 是否为纯粹的 javaScript 对象
// 只有通过对象直接量 {} 或 new Object() 创建的对象,才会返回 true
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 : []
}

// 递归合并 copy 到 clone 中,然后覆盖对象的同名属性
target[name] = jQuery.extend(deep, clone, copy)

// 如果不是深度合并,且不为 undefined,则直接覆盖目标的对象的同名属性
} else if (copy !== undefined) {
target[name] = copy
}
}
}
}
}

jQuery.buildFragment(args, nodes, scripts)

jQuery.buildFragment() 是一个私有函数,用来构建一个包含子节点 fragment 对象,但是关于 jQuery.buildFragment(args, nodes, scripts) 方法有一些需要注意的地方

  • 如果 HTML 代码符合缓存条件,则尝试从缓存对象 jQuery.fragments 中读取缓存的 DOM 元素
  • 创建文档片段 DocumentFragment
  • 调用方法 jQuery.clean(elems, context, fragment, scripts)HTML 代码转换为 DOM 元素,并存储在创建的文档片段中
  • 如果 HTML 代码符合缓存条件,则把转换后的 DOM 元素放入缓存对象 jQuery.fragments
  • 最后返回文档片段和缓存状态 {fragment: fragment, cacheable: cacheable}
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
67
68
69
70
71
// (1)定义函数 buildFragment
jQuery.buildFragment = function (args, nodes, scripts) {
// (2)定义局部变量,修正文档对象 doc
// 变量 fragment 指向稍后可能创建的文档片段 Document Fragment
// 变量 cacheable 表示 HTML 代码是否符合缓存条件
// 变量 cacheresults 指向从缓存对象jQuery.fragments 中取到的文档片段,其中包含了缓存的 DOM 元素
// 变量 doc 表示创建文档片段的文档对象
var fragment, cacheable, cacheresults, doc, first = args[0]

if (nodes && nodes[0]) {
// ownerDocument 表示 DOM 元素所在的文档对象,如果 ownerDocument 不存在,则假定 nodes[0] 为文档对象
doc = nodes[0].ownerDocument || nodes[0]
}

// 然后再次检查 doc.createDocumentFragment 是否存在
if (!doc.createDocumentFragment) {
doc = document
}

// (3)尝试从缓存对象 jQuery.fragments 中读取缓存的 DOM 元素
// html 代码需要满足下列所有条件,才认为符合缓存条件
if (args.length === 1 && typeof first === "string" &&
first.length < 512 &&
doc === document &&
first.charAt(0) === "<" &&

// 使用的正则方法如下
// rnocache = /<(?:script|object|embed|option|style)/i,
// checked = "checked" or checked
// rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i,
!rnocache.test(first) &&
(jQuery.support.checkClone || !rchecked.test(first)) &&
// var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" +
// "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video"
// var rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i")
(jQuery.support.html5Clone || !rnoshimcache.test(first))) {

// 如果为 true,则必须先复制一份再使用,否则可以直接使用
cacheable = true

// 读取缓存
cacheresults = jQuery.fragments[first]

if (cacheresults && cacheresults !== 1) {
fragment = cacheresults
}
}

jQuery.fragments = {}

// (4)转换 HTML 代码为 DOM 元素
// 创建文档片段
// 如果 !fragment 为 true,表示需要执行转换过程,有三种可能
// 1) html 代码不符合缓存条件
// 2) html 代码符合,但是是第一次转换,没有对应的缓存
// 3) html 代码符合,但是是第二次转换,对应的缓存值为 1
if (!fragment) {
fragment = doc.createDocumentFragment()
jQuery.clean(args, doc, fragment, scripts)
}

// (5)转换后的dom元素放入 jQuery.fragments
if (cacheable) {
jQuery.fragments[first] = cacheresults ? fragment : 1
}

// 返回文档片段和缓存状态 {fragment: fragment, cacheable: cacheable}
// fragment 中包含转换后的 dom 元素,cacheable 表示缓存状态
return { fragment: fragment, cacheable: cacheable }

}

jQuery.buildFragment() 的用法总结为

  • 如果 HTML 代码不符合缓存条件,则总是会执行转换过程
  • 如果 HTML 代码符合缓存条件,第一次转换后设置缓存值为 1,第二次转换后设置为文档片段,从第三次开始则从缓存中读取

jQuery.clean(elems, context, fragment, scripts)

方法 jQuery.clean(elems, context, fragment, scripts) 负责把 HTML 代码转换成 DOM 元素,并提取其中的 script 元素

  • 创建一个临时 div 元素,并插入一个安全文档片段中
  • HTML 代码包裹必要的父标签,然后用 innerHTML 赋值给临时 div ,从而将 HTML 代码转换为 DOM 元素,之后再层层剥去包裹的父元素,得到转换后的 DOM 元素
  • 移除 IE 6/7 自动插入的空 tbody 元素,插入 IE 6/7/8 自动剔除的前导空白符
  • 取到转换后的 DOM 元素集合
  • IE 6/7 中修正复选框和单选按钮的选中状态
  • 合并转换后的 DOM 元素
  • 如果传入了文档片段 fragment,则提取所有合法的 script 元素存入数组 scripts,并把其他元素插入文档片段 fragment
  • 最后返回转换后的 DOM 元素数组
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
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// (1)定义函数
// elems 数组,包含了待转换的 html 代码
// context 文档对象,被 buildFragment() 修正(doc),用于创建文本节点和临时 div
// fragment 文档片段,用于存放转换后的 dom 元素
// scripts 数组,用于存放转换后的 dom 元素中的 script 元素
clean: function(elems, context, fragment, scripts) {

var checkScriptType

// (2)修正(再次修正是是为了方便直接调用 jQuery.clean() 转换 HTML 代码为 DOM 元素)
context = context || document

if (typeof context.createElement === 'undefined') {
context = context.ownerDocument || context[0] && context[0].ownerDocument || document
}

// (3)遍历待转换的 HTML 代码数组 elems
// ret用于存放转换后的 dom 元素
var ret = [], j

// 在 for 语句的第 2 部分取出 elems[i] 赋值给 elem,并判断 elem 的有效性,传统的做法可能是比较循环变
// 量 i 与 elems.length,然后在 for 循环体中把 elems[i] 赋值给elem,再判断 elem 的有效性
// 另外,判断 elem 的有效性时使用的是 '!=',这样可以同时过滤 null 和 undefined,却又不会过滤整型数字 0
for (var i = 0, elem; (elem = elems[i]) != null; i++) {

// 如果是数值型,加上一个空字符串,即把 elem 转换为字符串
if (typeof elem === 'number') {
elem += ''
}

// 用于过滤空字符串,如果是数字 0,前面已经被转换为字符串 '0' 了,elem 为 false 则跳过本次循环
if (!elem) {
continue
}

// 若是 html 代码
if (typeof elem === 'string') {
// 创建文本节点
// 使用正则如下,作用是检测代码中是否含有标签,字符代码,数字代码
// rhtml = /<|&#?\w+;/
// 调用 document.cerateTextNode() 创建文本节点
if (!rhtml.test(elem)) {
elem = context.createTextNode(elem)

// 修正自关闭标签,使用正则如下
// rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig
} else {
elem = elem.replace(rxhtmlTag, '<$1></$2>')

// 创建临时 div 元素
// 使用正则如下
// rtagName = /<([\w:]+)/
// 提取 html 中标签,删除前导空白符和左尖括号
var tag = (rtagName.exec(elem) || ['', ''])[1].toLowerCase(),
wrap = wrapMap[tag] || wrapMap._default,
deoth = wrap[0],
div = context.createElement('div')

// 如果传入的文档对象 context 是当前文档对象,则把临时 div 插入 safeFragment 中
// 否则调用 createSafeFragment() 新建一个安全文档碎片(低版本浏览器也能识别的)在插入
if (context === document) {
safeFragment.appendChild(div)
} else {
createSafeFragment(context).appendChild(div)
}

// 包裹必要父元素,赋给临时 div
div.innerHTML = wrap[1] + elem + wrap[2]

// 用 while 循环层层剥去包裹的父元素,最终变量 div 将指向 HTML代码对应的 DOM 元素的父元素
while (depth--) {
div = div.lastChild
}

// 省略
// 移除 IE 6/7 自动插入的空 tbody 元素 ...
// 插入 IE 6/7/8 自动剔除的前导空白符 ...

// 取到转换后的 DOM 元素集合
elem = div.childNodes

// 省略
// 在 IE 6/7 中修正复选框和单选按钮的选中状态 ...

if (elem.nodeType) {
ret.push(elem)
} else {
ret = jQuery.merge(ret, elem)
}
}

}
}

// 如果传入文档片段 fragment 的情况
// 遍历数组 ret,提取 script 存入 [scripts],将其他元素插入文档片段 fragment
if (fragment) {
// 初始化函数 checkScriptType,用于检测 script 元素是否是可执行
// 使用正则如下
// rscriptType = /\/(java|ecma)script/i
checkScriptType = function (elem) {
return !elem.type || rscriptType.test(elem.type)
}
for (i = 0; ret[i]; i++) {
if (scripts && jQuery.nodeName(ret[i], 'script') && (!ret[i].type || ret[i].type.toLowerCase() === 'text/javascript')) {
scripts.push(ret[i].parentNode ? ret[i].parentNode.removeChild(ret[i]) : ret[i])
} else {
if (ret[i].nodeType === 1) {
var jsTags = jQuery.grep(ret[i].getElementsByTagName('script'), checkScriptType)

ret.splice.apply(ret, [i + 1, 0].concat(jsTags))
}
fragment.appendChild(ret[i])
}
}
}

// 返回数组 ret
// 但是要注意,如果传入了文档片段 fragment 和数组 scripts
// 那么调用 jQuery.clean() 的代码应该从文档片段 fragment 中读取转换后的 DOM 元素,并从数组 scripts 中读取合法的 script 元素
// 如果未传入,则只能使用返回值 ret
return ret
}

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×