jQuery技术内幕
丢笔记了有点可怜..重新写好了。书和代码一样重,拿在手上很有分量恩。
总体架构
|
自调用匿名函数
返回一个实例,省去了使用操作符 new;传入 window 对象,使 window 对象变为局部变量,访问 window 对象时不需要将作用域链回退到顶层作用域,并且便于压缩;传入 undefined 是为了避免 undefined 被修改
构造jQuery对象
构造函数
jQuery(selector [, context])
如果 selector 是简单的 “#ID”,且没有指定的上下文,则调用浏览器的.getElementById()
查找指定的元素;否则通过 jQuery 方法.find()
查找。.find()
会调用CSS选择器引擎 Sizzle 实现
jQuery(html[, ownerDocument])、jQuery(html, props)
如果 html 代码是一个简单标签,如<a></a>
,直接调用浏览器原生方法.createElement()
创建 DOM 元素;否则,通过浏览器的 innerHTML
实现,这个过程由方法 jQuery.buildFragment()
和方法jQuery.clean()
实现
第二个参数ownerDocument
用于指定创建新DOM元素的文档对象,默认是当前文档对象
如果 HTML 代码是一个单独的标签,第二个参数可以是 props。props 是包含属性和事件的对象,创建 DOM 元素后调用 jQuery 方法.attr()
将其设置到新创建的 DOM 元素上。可以包含以下特殊属性:html、text、data、width、height、offset、class,调用对应的 jQuery 方法
jQuery(element)、jQuery(elementArray)
将 DOM 元素封装到 jQuery 对象中并返回
jQuery(object)
将该对象封装到 jQuery 对象中并返回,允许调用 jQuery 方法
jQuery(callback)
在 document 上绑定一个 ready 事件监听函数。ready 事件并不是浏览器原生事件,而是DOMContentLoaded
事件、onreadystatuschange
事件和函数doScrollCheck()
的统称
jQuery(jQuery object)
创建该 jQuery 对象的副本并返回,副本与传入的 jQuery 对象引用相同的 DOM 元素
jQuery()
返回一个空的 jQuery 对象,可用于复用 jQuery 对象。例如,创建一个空得 jQuery 对象,在需要时手动修改其中的元素,再调用 jQuery 方法,避免重复创建 jQuery 对象
源码分析
- 属性 nodeType 声明了文档树种节点的类型,例如,Element 节点是 1,Text 节点是 3,Comment 节点是 8,Document 对象是 9,DocumentFragment 节点是 11
quickExpr 匹配 HTML 代码和 #ID
前一个规则匹配 HTML 标签,[^#<]
是为了防止$(location.hash)
情况下的 XSS 攻击;后一个规则匹配 #IDquickExpr = /(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/调用
.getElementById()
,在 Blackberry 4.6 会返回已经不在文档中的节点;检查 id 是否一致是因为在 IE6、IE7、某些版本的 Opera 中,可能会按属性 name 查找而不是 id
jQuery.buildFragment(args, nodes, scripts)
实现原理
创建一个文档片段 DocumentFragment,然后调用方法 jQuery.clean()
将 HTML 代码转换为 DOM 元素,并存储在创建的文档片段中。文档片段表示文档的一部分,但不属于文档树。把文档片段插入文档树时,不是插入 DocumentFragment 本身,而是它的所有子孙节点
源码分析
- 仅缓存使用两次以上的文档片段,且只缓存 DOM 元素,不缓存文本节点
- webkit 浏览器不会克隆
<options>
的checked
属性
jQuery.clean(elems, context, fragment, scripts)
实现原理
将 HTML 代码转换为 DOM 元素,并提取其中的 script 元素。该方法先创建一个临时的 div 元素,并将其插入一个安全文档片段中,然后把 HTML 代码赋值给 div 的innerHTML
,最后解析 div 元素的子元素得到转换后的 DOM 元素
源码分析
- 通过
ownerDocument
去检查 DOM 元素所在的文档对象 /<|&#?\w+;/
判断代码是否含有标签,字符代码或数字代码
特殊符号 | 字符代码 | 数字代码 | 备注 |
---|---|---|---|
“ | \" | \" | 双引号 |
‘ | \' | \' | 单引号 |
< | \< | \< | 小于 |
> | \> | \> | 大于 |
& | \& | \& | |
\ | \ | 空格 | |
© | \&cpoy; | \© | 版权符号 |
® | \® | \® | 注册符号 |
™ | \™ | \™ | 贸易符号 |
- IE9 及以下的浏览器无法序列化标签
<link>
和<script>
,解决方法是在外面包裹一层元素再转换 - 不支持 HTML5 的浏览器在使用未知标签之前调用
.createElement('unknown')
可以使浏览器正确解析和渲染这个未知标签
jQuery.extend()、jQuery.fn.extend()
使用
|
deep 是可选的布尔值,表示是否进行深度合并(即递归合并)。参数 target 是目标对象,如果只提供一个参数,那么jQuery
或jQuery.fn
会被当做目标对象,用于扩展方法和属性
源码分析
- 无法在基本类型上设置非原生属性
- 拷贝属性时没有使用
isOwnProperty
,不解 obj != null
可以判断 null 和 undefined 的情况
原型属性和方法
|
jQuery.each(collection, callback(indexInArary, valueOfElement))
静态方法 jQuery.each 是一个通用的遍历迭代方法,用于无缝遍历对象和数组。对于数组和含有 length 的类数组对象,该方法通过下标遍历;对于其他对象通过属性名遍历(for-in)。遍历过程毁掉函数返回 false,则结束遍历
源码分析
- 使用
for-in
遍历对象,但没有使用`hasOwnProperty
校验,将会遍历对象继承下来的属性。hasOwnProperty
是唯一不检查原型链的函数
jQuery.map(arrayObject, callback(value, indexOfKey))
静态方法jQuery.map()对数组中的每个元素或对象的每个属性调用一个回调函数,并将回调函数的返回值放入一个新的数组中
源码分析
.map() 中判断数组的条件如下
- length 大于 0,且 elems[0] 存在,且 elems[length - 1] 存在,则 elems 是一个类数组对象(鸭子模式?)
- length 等于 0
jQuery.isArray() 判断 elems 是真正的数组
空数组 [] 上调用方法
.concat()
扁平化结果集?
jQuery.pushStack(elements, name, arguments)
创建一个新的空 jQuery 对象,然后传入的元素集合放入这个 jQuery对象中,并保留对当前 jQuery 对象的引用
源码分析
- 通过 prevObject 巧妙地实现了栈
.end()
结束当前链条中最近的筛选啊从左,并将匹配元素集合还原为之前的状态。与.pushStack()
相比,相当于出栈
.eq(index)、.first()、.last()、.slice(start [, end])
方法.first()
和.last()
通过调用.eq(index)
实现,.eq()
通过调用.slice()
实现,`.slice()
调用方法.pushStack()
.push(value, …)、.sort([orderfunc])、splice(start, deleteCount, value, …)
指向同名的数组方法,因为它们的参数、功能和返回值与数组方法完全一致
静态属性和方法
|
类型检测
- 使用
object.toString.call(obj)
并从对象 class2tpye 中匹配获取对应的类型,省去截取字符串 - 1.7.2 版本通过检测特征属性 window 判断 window 对象
通过
isNaN
解析parseFloat()
的结果判断合法数字,并通过isFinite
判断是否有限isNumberic: function(obj) {return isNaN(parseFloat(obj)) && isFinite(obj);},
jQuery.isPlainObject(object)
- 通过 nodeType 判断 DOM 对象
for-in
中,非继承的属性会被优先枚举- 通过检测原型对象上的
isPrototypeOf
方法可以判断对象是否直接继承于 Object - constructor 默认引用构造函数,判断其是否属于非继承属性判断是否由被覆盖
jQuery.isEmptyObject(object)
for-in
会枚举非继承属性和从原型对象继承的属性。直接由 Object 继承的属性不可枚举
jQuery.parseJSON()
- 用正则 rvalidescape 把转移字符替换为 “@”
- 用正则 rvalidtokens 把字符串、true、false、null、数值替换为 “]”
- 用正则 rvalidbraces 删除正确地左方括号
- 用正则 rvalidchars 检查剩余字符是否只包含
],:{}\s
,是则认为 JSON 字符串合法 - 通过构建函数 Function() 创建函数对象并执行返回 json
jQuery.globalEval(code)
在全局作用域中执行代码,IE 中使用exexScript()
,其他浏览器通过闭包调用eval
实现
|
jQuery.camelCase(string)
将连字符式的字符串转换为驼峰式,用于 CSS 模块和数据缓存模块
|
jQuery.nodeName(elem, name)
检查 DOM 元素的节点名称(即属性 nodeName 与指定的值是否相等,检查时忽略大小写)
jQuery.trim(str)
移除字符串开头和结尾的空白符。在 IE9 以下的浏览器中,\s
不匹配不间断空格\A0
jQuery.makeArray(obj)
将一个类数组对象转换为真正的数组。判断传入参数是否是数组或类数组,否则将其插入一个空数组中返回
判断非数组的方法
- 没有属性 length
- 不是字符串类型,length 返回字符个数
- 不是函数类型,length 返回声明参数个数
- 不是 window 对象,length 返回窗口中的框架(frame,iframe)个数
- 不是正则对象,在黑莓4.7中,正则对象有 length 属性
可以通过array.call(arr, obj)
给类数组对象插入元素
jQuery.inArray(value, array [, fromIndex])
在素组中查找指定的元素并返回下标,未找到返回-1
jQuery.proxy(fn, context)
接受一个函数,返回一个新函数,新函数总是持有特定的上下文
|
jQuery.access(elems, key, value, exec, fn(elem, key, value), pass)
为集合中的元素设置一个或多个属性值,或者读取第一个元素的属性值
jQuery.uaMatch(ua)
解析用户代理navigator.userAgent
,并将解析结果重新封装为 jQuery.browser
选择器Sizzle
选择器表达式
- 块表达式
- 简单表达式
- ID
- CLASS
- TAG
- 属性表达式 ATTR
- 伪类表达式 PSEUDO
- 位置伪类 POS
- 子元素伪类 CHILD
- 内容伪类
- 可见伪类
- 表单伪类
- 简单表达式
- 块间关系符
- 父子关系 “>”
- 祖先后代关系 “ “
- 下一个兄弟元素 “+”
- 之后所有的兄弟元素 “~”
设计思路
- 处理选择器表达式:解析选择器表达式中的块表达式和块间关系符
- 处理块表达式:用块表达式的一部分查找,用剩余部分对查找结果过滤
- 处理块间关系符:按照块间关系符查找,用块表达式对查找结果进行过滤
从选择器表达式的执行过程的分析,还可以推到分析以下结论
- 从左到右的总体思路是不断缩小上下文,即不断缩小查找范围
- 从右到左的总体思路是先查找后过滤
- 在从左到右的查找过程中,每次处理块间关系符时都需要处理位置数量的子元素或后代元素,而从右到左的查找过程中,处理块间关系符时只需要处理单个父元素或有限数量的祖先元素。因此大多数情况下,采用从右到左的查找方式其效果要高于从左到右
Sizzle 是一款从右到左查找的选择器引擎,提供了与前面三个步骤相对于的核心接口
- 正则 chunker 负责从选择器表达式中提取块表达式和块间关系符
- 方法
Sizzle.find()
负责查找块表达式匹配的元素集合,方法Sizzle.filter()
负责用块表达式过滤元素集合 - 对象
Sizzleselector.relative
中的块间关系过滤函数根据块间关系符过滤元素集合,函数Sizzle()
则按照前面三个步骤将这些核心接口组织起来
Sizzle(seletor, contextm results, seed)
函数 Sizzle() 执行的六个关键步骤如下:
- 解析块表达式和块间关系符
- 如果存在位置伪类,则从左到右查找
- 查找第一个块表达式匹配的元素集合,得到第一个上下文元素集合
- 遍历剩余的块表达式和块间关系符,不断缩小上下文元素集合
- 否则从右到左查找
- 查找最后一个块表达式匹配的元素集合,得到候选集、映射集
- 遍历剩余的块表达式和块间关系符,对映射集执行块间关系过滤
- 根据映射集筛选候选集,将最终匹配的元素放入结果集
- 如果存在并列选择器表达式,则递归调用 Sizzle() 查找匹配的元素集合,并合并、排序、去重
Sizzle.selectors.relative
对象 Sizzle.selectors.relative 中存放了块间块间关系符和对应的块间关系过滤函数,称为“块间关系过滤函数集”
选择器表达式 | 说明 | 从右到左过滤方式 | |
---|---|---|---|
“” | ancestor descendant | 匹配后代元素 | 检查祖先元素是否匹配错左侧的块表达式 |
“+” | prev + next | 匹配下一个兄弟元素 | 检查前一个兄弟元素是否匹配左侧的块表达式 |
“>” | parent > child | 匹配所有子元素 | 检查父元素是否匹配左侧的块表达式 |
“-“ | prev ~ siblings | 匹配之后的所有兄弟元素 | 检查之前的兄弟元素是否匹配左侧的块表达式 |
|
关键步骤如下:
- 遍历映射集 checkSet
- 按照块间关系符查找每个元素的兄弟元素、父元素或祖先元素
- 检查找到的元素是否匹配参数 part, 并替映射集 checkSet 中对应位置的元素
- 如果参数 part 是标签字符串,则检查找到的元素其节点名称 nodeName 是否与其相同。是的话替换为找到的元素,否则替换为 false
- 如果参数 part 是 DOM 元素,则检查找到的元素是否预置相等,是的话替换为 true,否则替换为 false
- 如果参数 part 是非标签字符串,则调用方法 Sizzle.filter 过滤。也就是说,遍历结束后,映射集 checkSet 中得元素可能会是兄弟元素、父元素、祖先元素、true 或 false
“+”
如果参数 part 是标签字符串或者 DOM 元素,则通过 elem.previousSibling
遍历 checkSet 中每个元素之前的兄弟节点。
遍历结束后,映射集 checkSet 中的元素可能是兄弟元素,true 或 false
“>”
如果参数 part 是标签字符串,遍历 checkSet 每个元素的父节点判断是否一致,返回父节点或 false
- 如果参数 part 是 DOM,遍历父节点判断是否一致,返回 true 或 false;如果参数 part 是非标签字符串,遍历并替换为父节点,再调用
Sizzle.filter
过滤
遍历结束后,映射集 checkSet 中的元素可能是父亲元素、true 或 false
“ “
如果参数 part 是非标签字符串或 DOM 元素,则调用函数 dirCheck() 过滤映射集 checkSet;如果参数 part 是标签字符串,调用函数 dirNodeCheck() 过滤映射集 checkSet
|
“~”
实现与 “ “ 类似,传入 checkFn 的参数不同
dirCheck(dir, cur, doneName, checkSet, nodeCheck, isXML)
遍历候选集 checkSet,检查每个元素在某个方向 dir 上是否有与参数 cur 匹配或相等的元素。使用 Sizzle.filter 过滤元素
dirNodCheck(dir, cur, doneName, checkSet, nodeCheck, isXML)
遍历候选集 checkSet,检查其中的元素在某个方向 dir 上是否有与参数 cur 匹配的元素。缓存等步骤与 dirCheck 一致,过滤元素的操作不同。只有当参数 part 是标签字符串时,才会调用函数 dirNodeCheck()
Sizzle.selectors.order
定义查找单个块表达式时的查找顺序,依次是 ID、CLASS、NAME、TAG。其中,CLASS 需要浏览器支持
Sizzle.selecotrs.match/leftMatch
存放了表达式类型和正则的映射,正则用于确定块表达式的类型,并解析其中的参数
|
为对象 Sizzle.sectors.match 中的正则增加一段后缀正则,然后加上一段前缀正则来构造对象 Sizzle.selectors.leftMatch 的同名正则
后缀正则 /(?![^\[]*\])(?![^\(]*\))/
要求接下来的字符不能含有 “]”,”)”
|
前缀正则 /(^(?:.|\r|\n)*?)/
捕获匹配正则的表达式前面的字符,主要是捕获转义反斜杠 “\“,以支持将特殊字符作为普通字符使用
|
Sizzle.selecotrs.match.ID
用于匹配简单表达式 “#id”,并解析 “#” 之后的字符串
Sizzle.selecotrs.match.CLASS
用于匹配简单表达式 “.class”,并解析 “.” 之后的字符串
Sizzle.selecotrs.match.NAME
用于匹配属性表达式 “[name=”value”]”,并解析属性 name 的值
Sizzle.selecotrs.match.ATTR
分组 | 内容 |
---|---|
1 | 属性名 |
2 | 等号部分 |
3 | 引号 |
4 | 属性值 |
5 | 无引号时的属性值 |
Sizzle.selecotrs.match.TAG
用于匹配简单表达式 “tag”,并解析标签名
Sizzle.selecotrs.match.CHILD
用于匹配子元素伪类表达式 :nth-child(index/even/odd/equation),:first-child,:last-child、:only-child,并解析子元素伪类和伪类参数
分组 | 内容 |
---|---|
1 | 伪类 |
2 | 伪类参数 |
|
Sizzle.selecotrs.match.POS
用于匹配子元素伪类表达式 “:eq(index)”、 “:gt(index)”、 “:lt(index)”、 “:first”、 “:last”、 “:odd”、 “:even”,并解析位置伪类和伪类参数
分组 | 内容 |
---|---|
1 | 伪类 |
2 | 伪类参数 |
|
Sizzle.selecotrs.match.PSEUDO
用于匹配伪类表达式,解析 “:” 之后的伪类和伪类参数,含有三个分组:伪类、引号、伪类参数
|
Sizzle.selectors.find
定义了 ID、CLASS、NAME、TAG 所对应的查找函数,称为“查找函数集”。返回元素集合或 undefined,内部通过调用相应的原生方法。其中,CLASS 需要浏览器支持方法 getElementsByClassName()
Sizzle.selectors.preFilter
定义了类型 CLASS、ID、TAG、CHILD、ATTR、PSEUDO、POS 所对应的预过滤函数,称为“预过滤函数集”。在方法 Sizzle.filter(expr, set, inplacec, not) 中,预过滤函数在过滤函数 Sizzle.selectors.filter[type] 之前被调用,修正与过滤操作相关的参数
|
类型 | 修正行为 |
---|---|
CLASS | 过滤不匹配元素,或缩小元素集合 |
ID | 过滤转义反斜杠 |
TAG | 过滤转义反斜杠,转为小写 |
CHILD | 格式化子元素伪类参数 |
ATTR | 修正属性名和属性值 |
PSEUDO | 处理 :not(selector) 的伪类参数 |
POS | 修正位置伪类的参数下标 |
三种可能的返回值说明如下
返回值 | 说明 |
---|---|
false | 已经执行过滤,或者已经缩小候选集,不需要再执行过滤函数,例如 CLASS |
true | 需要继续执行其他的预过滤函数,尚不到执行过滤函数的适合,例如 PSEUDO 预过滤函数中遇到 POS、CHILD 时 |
其他 | 可以调用对应的过滤函数 |
过滤函数
- CLASS
在 className 前后加上空格,然后通过 indexOf()
判断是否匹配 - ID
过滤反斜杠,并返回 id 值 - TAG
过滤反斜杠并返回转为小写的标签名 CHILD
将伪类 :nth-child(index/even/odd/equation) 的参数格式化为 first*n + lastvar test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec(match[2] === "even" && "2n" ||match[2] === "odd" && "2n+1" ||!/\D/.test(match[2]) && "0+" + match[2] ||match[2]);match[2] = (test[1] + (test[2] || 1)) - 0; // firstmatch[3] = test[3] - 0; // lastATTR
过滤属性值的反斜杠,修正 name 参数PSEUDO
伪类过滤函数,处理 :not(selector) 的情况,将匹配元素中 match[3](伪类参数 selectors)替换为与之匹配的元素集合;对于位置伪类和子元素伪类继续执行各自对应的预过滤函数POS
位置伪类过滤函数,负责在匹配结果 match 的头部插入一个新元素 true,使得匹配结果 match 中位置伪类参数的下标变为了3,从而与伪类的匹配结果保持一致
Sizzle.selectors.filters
定义了一组伪类和对应的伪类过滤函数,称为“伪类过滤函数集”,调用的方法链为 Sizzle.filter => Sizzle.selectors.PSEUDO() => Sizzle.selectors.filters。通过检查元素 nodeName 和 type 属性,或者其他相关属性,检查元素是否匹配伪类,返回一个布尔值。不包含伪类 :not() 和 :contains() 的处理。参数格式为:
|
Sizzle.selectors.setFilters
定义了一组位置伪类和对应的伪类过滤函数,称为“伪类过滤函数集”,调用的方法连为:Sizzle.filter() => Sizzle.selectors.filter.POS() => Sizzle.selectors.setFilters()。负责检查元素是否匹配过滤表达式,通过下标参数确定元素在集合中的位置,返回一个布尔值,参数格式为:
|
Sizzle.selectors.filter
定义了类型 PSEUDO、 CHILD、 ID、 TAG、 CLASS、 ATTR、 POS 所对应的过滤函数,称为“过滤函数集”。方法调用链为:Sizzle.filter() => Sizzle.selectors.filter[type]。负责检查元素是否匹配过滤表达式,返回一个布尔值
|
Sizzle.selectors.filter.PSEUDO
检查元素是否匹配伪类,大部分检查通过调用伪类过滤函数集 Sizzle.selectors.filters 中对应的伪类过滤函数来实现,对于伪类 :contains(text)、 :not(selector) 则做特殊处理
Sizzle.selectors.filter.CHILD
检查元素是否匹配子元素伪类,:first() 和 :last() 通过兄弟节点判断,:nth-child() 通过子节点的下标判断
Sizzle.selectors.filter.ID
检查元素的属性 id 是否匹配
Sizzle.selectors.filter.TAG
检查元素的 nodeName 是否匹配
Sizzle.selectors.filter.CLASS
在 cassName 前后加空格,然后判断 indexOf() 的返回值
Sizzle.selectors.ATTR
通过 Sizzle.attr() 读取属性并判断是否匹配。在 jQuery 中,Sizzle.attr() 等价于 jQuery.attr(),因此总返回 HTML 属性;在独立使用 Sizzle 时,则先尝试读取 DOM 属性,如果不存在才会读取 HTML 属性
Sizzle.selectors.POS
位置伪类过滤函数,通过调用位置伪类过滤函数集 Sizzle.selectors.setFilters 中对应的位置伪类过滤函数来实现
工具方法
Sizzle.uniqueSort(results)
使用 sortOrder() 对数组中的元素进行排序并排重
sortOrder(a, b)
复制比较元素 a 和元素 b 在文档中的位置。通过调用原生方法 compareDocumentPosition() 或者原生属性 sourceIndex 来实现。前者用于比较两个元素的文档位置,后者返回元素在文档中的序号,返回值等于该元素在document.getElementsByTagName('*')
中的位置