[TOC]
##1.理解架构
###1.1 最新jQuery2.1.1版本的结构:
;(function(global, factory) { |
###1.2 jQuery模块依赖
###1.3 jQuery的类数组对象结构
通过 $(".Class")
构建的对象结构如下所示:
整个结构很明了,通过对象键值对的关系保存着属性,原型保存着方法。我们来简单的模拟一个这样的数据结构:
var aQuery = function(selector) { |
函数 aQuery() 内部首先保证了必须是通过 new
操作符构建。这样就能保证当前构建的是一个带有 this
的实例对象,既然是对象我们可以把所有的属性与方法作为对象的key与value的方式给映射到this上,所以如上结构就可以模拟出jQuery的这样的操作了,即可通过索引取值,也可以链式方法取值,但是这样的结构是有很大的缺陷的,每次调用ajQuery方法等于是创建了一个新的实例,那么类似get方法就要在每一个实例上重新创建一遍,性能就大打折扣,所以jQuery在结构上的优化不仅仅只是我们看到的,除了实现类数组结构、方法的原型共享,而且还实现方法的静态与实例的共存。
###1.4 jQuery中ready与load事件
jQuery有3种针对文档加载的方法:
$(document).ready(function() { |
DOM文档加载的步骤:
- 解析HTML结构。
- 加载外部脚本和样式表文件。
- 解析并执行脚本代码。
- 构造HTML DOM模型。//ready
- 加载图片等外部文件。
- 页面加载完毕。//load
ready与load的区别就在于资源文件的加载,ready构建了基本的DOM结构,所以对于代码来说应该越快加载越好。ready在第 4 步完成之后就执行了,但是load要在第 6 步完成之后才执行。
###1.5 jQuery多库共存处理
引入jQuery运行这个 noConflict
函数将变量 $
的控制权让给第一个实现它的那个库,确保jQuery不会与其他库的$对象发生冲突。
在运行这个函数后,就只能使用jQuery变量访问jQuery对象。
Example:
//让出控制权 |
实现原理:
Var _jQuery = window.jQuery, |
jQuery在占用 $
时,已经把之前的存在的命名空间给缓存起来, noConflict
通过对比当前的命名空间达到交换的目的: 首先,先判断下当前的 $
空间是不是被jQuery接管了,如果是则让出控制权给之前的 _$
引用的库,如果传入 deep
为 true
的话等于是把 jQuery 的控制权也让出去了。
##2.核心模块
###2.1 分离构造器
通过new操作符构建一个对象,一般经过四步:
1.创建一个新对象
2.将构造函数的作用域赋给新对象(所以this就指向了这个新对象)
3.执行构造函数中的代码
4.返回这个新对象
最后一点就说明了,我们只要返回一个新对象即可。其实new操作符主要是把原型链跟实例的this关联起来,这才是最关键的一点,所以我们如果需要原型链就必须要new操作符来进行处理。否则this则变成window对象了。
我们来剖析下jQuery的这个结构,以下是我们常见的类式写法:
var $$ = ajQuery = function(selector) { |
首先改造jQuery无new的格式,我们可以通过instanceof判断this是否为当前实例:
var $$ = ajQuery = function(selector) { |
但是注意千万不要像下面这样写:
//错误 这样会无限递归自己,从而造成死循环并且溢出。 |
jQuery为了避免出现这种死循环的问题,采取的手段是把原型上的一个init方法作为构造器
var $$ = ajQuery = function(selector) { |
这样确实解决了循环递归的问题,但是又问题来了,init是ajQuery原型上作为构造器的一个方法,那么其this就不是ajQuery了,所以this就完全引用不到ajQuery的原型了,也就无法引用到ajQuery原型上定义的一系列方法。
###2.2 静态与实例方法共享设计
上一节提到分离后的构造器无法访问ajQuery原型上定义的一系列方法,jQuery给出如下方案:
ajQuery.fn = ajQuery.prototype = { |
这段代码就是整个结构设计的最核心的东西了,
把jQuery.prototype原型的引用赋给jQuery.fn.init.prototype的原型,这样就把2个构造器的原型给关联起来了,整个结构就活了!不得不佩服作者的设计思路,别具匠心。
init的构造图:
通过原型传递解决问题,把jQuery的原型传递给jQuery.prototype.init.prototype。换句话说jQuery的原型对象覆盖了init构造器的原型对象,因为是引用传递所以不需要担心这个循环引用的性能问题。
###2.3 插件接口的设计
jQuery插件的开发分为两种:
- 一种是挂在jQuery命名空间下的全局函数,也可称为静态方法;
- 另一种是jQuery对象级别的方法,即挂在jQuery原型下的方法,这样通过选择器获取的jQuery对象实例也能共享该方法。
提供的接口:$.extend(target, [object1], [objectN])
jQuery的代码实现:
jQuery.extend = jQuery.fn.extend = function() { |
从jQuery的源码中可以看到,jQuery.extend和jQuery.fn.extend其实是同指向同一方法的不同引用。
这里有一个设计的重点,通过调用的上下文,我们来确定这个方法是作为静态还是实例处理:
- jQuery.extend 调用的时候,this是指向jQuery对象,所以这里扩展在jQuery上,作为静态方法。
- jQuery.fn.extend 调用的时候,this指向jQuery.fn对象,jQuery.fn 和jQuery.prototype指向同一对象,扩展fn就是扩展jQuery.prototype原型对象,因此这里增加的是原型方法,也就是对象方法了。
接口的使用:
//挂在jQuery命名空间下的全局函数,扩展后可以通过jQuery.data()和jQuery.removeData()访问, |
注意:
根据传入的参数不同,当参数仅仅传入一个对象时是对jquery功能的扩展,如果传入多个对象时,将用扩展传入的第一个对象,此过程与本节内容无关,不再讨论。
###2.4 回溯处理
jQuery每次dom操作会返回一个jQuery对象而非dom对象,jQuery对象实际上是对dom对象的一层包装,每个jQuery对象都有三个属性:context
、selector
和 prevObject
,其中的 prevObject
属性就指向这个对象栈中的前一个对象,而通过这个属性可以回溯到最初的DOM元素集中。
在浏览器可以看到jQuery对象属性:
jQuery内部维护着一个jQuery对象栈。每个遍历方法都会找到一组新元素(一个jQuery对象),然后jQuery会把这组元素推入到栈中。
jQuery为我们操作这个内部对象栈提供个非常有用的2个方法:
.end() //回溯到之前的Dom元素集合 |
end()
方法主要用于jQuery的链式属性中。当没有使用链式用法时,我们通常只是调用变量名上的前一个对象,所以我们不需要操作栈。
Exapmple:
<ul class="first"> |
//设置.first .foo颜色黑色,.first .foo颜色红色 |
jQuery的代码实现:
//可以看到end方法返回prevObject属性 |
流程解析:
- 首先构建一个新的jQuery对象,因为constructor是指向构造器的,所以这里就等同于调用jQuery()方法了,返回了一个新的jQuery对象;
- 然后用jQuery.merge语句把elems节点合并到新的jQuery对象上;
- 最后给返回的新jQuery对象添加prevObject属性,我们看到prevObject其实还是当前jQuery的一个引用罢了,所以也就是为什么通过prevObject能取到上一个合集的原因了。
addBack()
和 addSelf()
方法与 end()
类似,多了一步合并当前对象的操作,不再赘述。
add: function( selector, context ) { |
###2.5 迭代器
迭代器是一个框架的重要设计。我们经常需要提供一种方法顺序用来处理聚合对象中各个元素,而又不暴露该对象的内部,这也是设计模式中的迭代器模式(Iterator)。针对迭代器,有几个特点:
- 访问一个聚合对象的内容而无需暴露它的内部。
- 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
- 遍历的同时更改迭代器所在的集合结构可能会导致问题。
除此之外,还要考虑支持以下至少四种情况:
- 支持聚合对象,可能是对象,字符串或者数组等类型
- 支持参数传递
- 支持上下文的传递
- 支持循环中退出
jQuery.each()
实现:
each: function( obj, callback, args ) { |
jQuery的each方法从使用上就要分2种:
- $.each()
- $(selector).each()
$(selector).each()
的实现
each: function(callback, args) { |
jQuery可以是多个合集数组DOM,所以在处理的时候经常就针对每一个DOM都要单独处理,所以一般都需要调用 $(selector).each()
方法,接口的抽象合并在jQuery内部的运用很多,把相同功能的代码功能合并处理。
##3.回调Callbacks
###3.1 运用回调
我们经常会在这些情况使用函数回调:
- 事件触发通知
- 资源加载通知
- 定时器延时
- ajax、动画通知等等。
以上都是很单一的事件监听回调的处理方式,但是jQuery把回调函数的用法设计成一个更高的抽像,用于解耦与分离变化。
例如:
jQuery针对Dom的处理提供了append
、prepend
、before
、after
等方法的处理,这几个方法的特征:
- 参数的传递可以是HTML字符串、DOM元素、元素数组或者jQuery对象
- 为了优化性能针对节点的处理需要生成文档碎片
实现代码:
//callback 省略 |
jQuery通过抽象出一个domManip方法,然后在这个方法中处理共性,合并多个参数的处理与生成文档碎片的处理,然后最终把结果通过回调函数返回给每一个调用者。
###3.2 观察者模式
观察者模式也即发布/订阅模式的背后,总的想法是在应用程序中增强松耦合性。并非是在其它对象的方法上的单个对象调用。一个对象作为特定任务或是另一对象的活动的观察者,并且在这个任务或活动发生时,通知观察者。
观察者的使用场合就是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。
JS里对观察者模式的实现是通过回调来实现的,我们来先定义一个Observable对象,其内部包含了2个方法:订阅add方法与发布fire方法,如下代码:
var Observable = { |
使用add开始订阅:
Observable.add(function() { |
使用fire开始发布:
Observable.fire(); // 1, 2 |
###3.3 观察者模式运用
假设一段ajax的请求,成功后通过done返回结果数据:
$.ajax({ |
所有的逻辑都写在done方法里面,虽然可以使用,但是问题就是逻辑太复杂了。Done里面有数据处理、html渲染、还可能有其它不同场景的业务逻辑。这样如果是换做不同的人去维护代码,增加功能就会显得很混乱而且没有扩展性。那么观察者模式能很好的解决了这个的问题。
试着优化代码:
$.ajax({ |
这种方式的好处是,分离出各种的业务函数,从而降低了代码之间的耦合度,但是这样代码写法几乎就是“就事论事”的处理,达不到抽象复用。
使用之前的观察者模式再次优化上面的代码:
Observable.add(function() { |
设计该模式背后的主要动力是促进形成松散耦合。在这种模式中,并不是一个对象调用另一个对象的方法,而是一个对象订阅另一个对象的特定活动并在状态改变后获得通知。订阅者也称为观察者,而被观察的对象称为发布者或主题。当发生了一个重要的事件时,发布者将会通知(调用)所有订阅者并且可能经常以事件对象的形式传递消息。
###3.4 jQuery回调模块Callbacks
$.Callbacks是一个工厂函数,使用函数调用创建对象,它有一个可选参数 flags
用来设置回调函数的行为,对外的接口也就是self的返回。
jQuery.Callbacks()的API:
callbacks.add()
: 回调列表中添加一个回调或回调的集合。callbacks.disable()
: 禁用回调列表中的回调。callbacks.disabled()
: 确定回调列表是否已被禁用。callbacks.empty()
: 从列表中删除所有的回调。callbacks.fire()
: 用给定的参数调用所有的回调。callbacks.fired()
: 访问给定的上下文和参数列表中的所有回调。callbacks.fireWith()
: 访问给定的上下文和参数列表中的所有回调。callbacks.has()
: 确定列表中是否提供一个回调。callbacks.lock()
: 锁定当前状态的回调列表。callbacks.locked()
: 确定回调列表是否已被锁定。callbacks.remove()
: 从回调列表中的删除一个回调或回调集合。
参数列表:
once
: 确保这个回调列表只执行( .fire() )一次(像一个递延 Deferred)。memory
: 保持以前的值,将添加到这个列表的后面的最新的值立即执行调用任何回调 (像一个递延 Deferred)。unique
: 确保一次只能添加一个回调(所以在列表中没有重复的回调)。stopOnFalse
: 当一个回调返回false 时中断调用。
实现代码:
jQuery.Callbacks = function(options) { |
整个结构要分三部分:
- Options参数缓存
- 内部fire触发器的设计
- 外部
参数的缓存设计:
// Callbacks是可以是接受的字符串的组合传参数,可以使用空格分割, |
接口的设计:
callback需要在内部维护着一个list的队列数组,用于保存订阅的对象数据。同时也需要提供了add、remove、fire等订阅、发布、删除类似的接口。
可以构建一个存放回调的数组,如 var list = []
,通过闭包使这条回调数组保持存在。添加回调时,将回调push进list,执行则遍历list执行回调。
##4.数据缓存
##5.deferred.js
###5.1 Deferred介绍
Deferred对象是由$.Deferred构造的,它用来解决JS中的异步编程,它遵循 Common Promise/A 规范。实现此规范的还有 when.js 和 dojo。
Deferred 提供了一个抽象的非阻塞的解决方案(如异步请求的响应),它创建一个promise对象,其目的是在未来某个时间点返回一个响应。简单来说就是一个异步/同步回调函数的处理方案。
$.Deferred作为新特性首次出现在版本1.5中,这个版本利用Deferred又完全重写了Ajax模块。$.Deferred在jQuery代码自身四处被使用,分别是promise方法、DOM ready、Ajax模块、动画模块。
deferred.js为jQuery对象提供了两个静态方法: $.Deferred()
、 $.when()
。
jQuery的Deferred就是模块化程度非常高,可以混入任意的对象接口中配合使用:
function task(name) { |
###5.2 用法
done/resolve
处理成功状态, fail/reject
处理失败状态, progress/notify
当前处理进度状态
调用示例:
function fn1() { |
then
,一次添加成功,失败,进度回调函数,调用示例:
var deferred = $.Deferred(); |
调用then后还可以继续链式调用then添加多个不同回调函数,这个then也正是jQuery对 Common Promise/A 的实现。
var deferred = $.Deferred(); |
使用always方法为成功,失败状态添加同一个回调函数,回调函数中可以使用deferred.state方法获取异步过程中的最终状态,调用示例:
var deferred = $.Deferred() |
$.when()
保证多个异步操作全部成功后才回调,调用示例:
deferred.promise()
没有参数时,返回一个新的deferred对象;接受参数时,作用为在参数对象上部署deferred接口返回只能添加回调的对象,对象的运行状态无法被改变,即这个对象与$.Deferred()返回的对象不同,只能done/fail/progress,不能resolve/reject/notify。
###5.3 $.Deferred接口
$.Deferred的源码中对动作接口的定义:
[ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ],
[ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ],
[ "notify", "progress", jQuery.Callbacks("memory") ]
实质上是观察者模式的实现:
|| 订阅方法 || 发布方法 ||
|| Done (操作完成) || resolve/resolveWith(解决) ||
|| Fail (操作失败) || reject/rejectWith(拒绝) || 30 ||
|| Progress (操作进行中) || notify/notifyWith(通知) ||
With接口扩展了3个可以定义上下文的发布方法
$.when
$.when接受若干个对象,参数仅一个且非Deferred对象将立即执行回调函数,Deferred对象和非Deferred对象混杂时,对于非Deferred对象remaining减1
Deferred对象总数 = 内部构建的Deferred对象 + 所传参数中包含的Deferred对象,所传参数中所有Deferred对象每当resolve时remaining减1,直到为0时(所有都resolve)执行回调