JQuery源码分析(八)

时间:2023-03-09 09:34:09
JQuery源码分析(八)

jQuery的each迭代器

jQuery的each方法从使用上就要分2种情况:

 $.each()函数
$(selector).each()

  

$.each()函数和$(selector).each()是不一样的,后者是专门用来遍历一个jQuery对象的,是为jQuery内部服务的。

$.each()函数可用于迭代任何集合,无论是“名/值”对象(JavaScript对象)或数组。在迭代数组的情况下,回调函数每次传递一个数组索引和相应的数组值作为参数。

(该值也可以通过访问this关键字得到,但是JavaScript始终将this值作为一个Object,即使它是一个简单的字符串或数字值。)该方法返回其第一个参数,这是迭代的对象。

jQuery的实例方法最终也是调用的静态方法。

其中each的实例方法如下:

可见内部是直接调用的静态方法:

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

  

jQuery.each静态方法:

each: function(obj, callback, args) {
var value,
i = 0,
length = obj.length,
isArray = isArraylike(obj); if (args) {
if (isArray) {
for (; i < length; i++) {
value = callback.apply(obj[i], args); if (value === false) {
break;
}
}
} else {
for (i in obj) {
value = callback.apply(obj[i], args); if (value === false) {
break;
}
}
}

  

实现原理几乎一致,只是增加了对于参数的判断。对象用for in遍历,数组用for遍历。

jQuery可以是多个合集数组DOM,所以在处理的时候经常就针对每一个DOM都要单独处理,所以一般都需要调用this.each 方法,如下代码:

dequeue: function( type ) {
return this.each(function() {
jQuery.dequeue( this, type );
});
},

  

迭代器除了单纯的遍历,在jQuery内部的运用最多的就是接口的抽象合并,相同功能的代码功能合并处理:

例如一:

jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
class2type[ "[object " + name + "]" ] = name.toLowerCase();
});

例如二:

jQuery.each({
mouseenter: "mouseover",
mouseleave: "mouseout",
pointerenter: "pointerover",
pointerleave: "pointerout"
}, function( orig, fix ) {
//处理的代码
});

  

可以看出上面代码方法,针对相同的功能,节约了大量的代码空间。

实例代码:

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="http://img.mukewang.com/down/540812440001e40e00000000.js" type="text/javascript"></script>
<title></title>
</head>
<body> <script type="text/javascript"> jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
$('body').append(name.toLowerCase()+'</br>')
}); </script>
</body>
</html>

  

理解回调函数

函数是第一类对象,这是javascript中的一个重要的概念。意味着函数可以像对象一样按照第一类管理被使用,所以在javaScript中的函数:

能“存储”在变量中

能作为函数的实参被传递

能在函数中被创建

能从函数中返回

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针调用它所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件条件进行响应。

因此从上面可以看出来,回调本质上是一种设计原则,并且jQuery的设计原则遵循了这个模式。

在后端的编程语言中,传统函数以参数形式输入数据,并且使用返回语句返回值。理论上,在函数结尾处有一个return返回语句,结构上就是:一个输入和一个输出。简单的理解函数本质上就是输入和输出之间实现过程的映射

但是,当函数的实现过程非常漫长,你是选择等待函数完成处理,还是使用回调函数进行异步处理呢?这种情况下,使用回调函数变得至关重要,例如:AJAX请求。若是使用回调函数进行处理,代码就可以继续进行其他任务,而无需空等。实际开发中,经常在javascript中使用异步调用。

jQuery中遍地都是回调的设计:

异步回调:

事件句柄回调

$(document).ready(callback);
$(document).on(‘click’,callback)

  

Ajax异步请求成功失败回调

$.ajax({
url: "aaron.html",
context: document
}).done(function() {
//成功执行
}).fail(function() {
//失败执行
);

  

动画执行完毕回调:

$('#clickme').click(function() {
$('#book').animate({
opacity: 0.25,
left: '+=50',
height: 'toggle'
}, 5000, function() {
// Animation complete.
});
});

  

以上都是jQuery的回调直接运用,运用基本都是将匿名函数作为参数传递给了另一个函数或方法。而且以上都有一个特点,执行的代码都是异步的。

同步回调:

当然回调不仅仅只是处理异步,一般同步(很耗时的任务)的场景下也经常用到回调,比如要求执行某些操作后执行回调函数。

一个同步(阻塞)中使用回调的例子,目的是在test1代码执行完成后执行回调callback

var test1 = function(callback) {
//执行长时间操作
callback();
}
test1(function() {
//执行回调中的方法
});

  

所以理解回调函数最重要的2点:

1、一个回调函数作为参数传递给另一个函数是,我们仅仅传递了函数定义。我们并没有在参数中执行函数。我们并不传递像我们平时执行函数一样带有一对执行小括号()的函数

2、回调函数并不会马上被执行,它会在包含它的函数内的某个特定时间点被“回调”。

实例代码:

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="http://img.mukewang.com/down/540812440001e40e00000000.js" type="text/javascript"></script>
<script src="http://img.mukewang.com/down/541f6ff70001a0a500000000.js" type="text/javascript"></script> <title></title>
</head>
<body> <div id="target">
点击触发事件回调
</div> <img id="book" alt="" width="100" height="123"
style="position: relative; left: 10px;background:#ccc" /> <div id="clickme">
点击动画等待动画结束后回调
</div> <script type="text/javascript"> //同步回调
function callback(args, fn) {
var args = args * 2;
fn(args);
} callback(2, function(value) {
show('同步回调->'+ value)
}) //异步事件回调
$("#target").click(function() {
show("异步事件回调");
}); //异步动画回调
$('#clickme').click(function() {
$('#book').animate({
opacity: 0.25,
left: '+=50',
height: 'toggle'
}, 1000, function() {
show('异步动画回调')
});
});
</script>
</body>
</html>

  

回调的灵活运用

我们经常会这样使用函数回调:

事件触发通知

资源加载通知

定时器延时

ajax、动画通知等等。

以上都是很单一的事件监听回调的处理方式,但是jQuery把回调函数的用法设计成一个更高的抽像,用于解耦与分离变化。

例子一:

jQuery针对Dom的处理提供了append、prepend、before、after等方法的处理,这几个方法的特征:

1、参数的传递可以是HTML字符串、DOM元素、元素数组或者jQuery对象

2、为了优化性能针对节点的处理需要生成文档碎片

可见几个方法都是需要实现这2个特性的,那么我们应该如何处理?

高层接口:

before: function() {
return this.domManip(arguments, function(elem) {
if (this.parentNode) {
this.parentNode.insertBefore(elem, this);
}
});
}, after: function() {
return this.domManip(arguments, function(elem) {
if (this.parentNode) {
this.parentNode.insertBefore(elem, this.nextSibling);
}
});
},

底层实现:

domManip: function(args, callback) {
// Flatten any nested arrays
args = concat.apply([], args);
// We can't cloneNode fragments that contain checked, in WebKit
if (isFunction ||
//多参数处理
self.domManip(args, callback);
} if (l) {
//生成文档碎片
fragment = jQuery.buildFragment(args, this[0].ownerDocument, false, this);
callback.call(this[i], node, i);
}
return this;
}

我们观察下jQuery的实现,通过抽象出一个domManip方法,然后在这个方法中处理共性,合并多个参数的处理与生成文档碎片的处理,然后最终把结果通过回调函数返回给每一个调用者。

例子二:

在很多时候需要控制一系列的函数顺序执行。那么一般就需要一个队列函数来处理这个问题。

我们看一段代码:

function Aaron(List, callback) {
setTimeout(function() {
var task;
if (task = List.shift()) {
task(); //执行函数
}
if (List.length > 0) { //递归分解
arguments.callee(List)
} else {
callback()
}
}, 25)
} //调用
​Aaron([
function() {
alert('a')
},
function() {
alert('b')
},
function() {
alert('c')
}
], function() {
alert('callback')
}) // 分别弹出 ‘a’ , ‘b’ ,'c',’callback

传入一组函数参数,靠递归解析,分个执行,其实就是靠setTimeout可以把函数加入到队列末尾才执行的原理,这样的写法就有点就事论事了,聚合对象完全是一个整体,无法再次细分出来,所以我们需要一种方案,用来管理分离每一个独立的对象。

我们换成jQuery提供的方式:

var callbacks = $.Callbacks();
callbacks.add(function() {
alert('a');
})
callbacks.add(function() {
alert('b');
})
callbacks.fire(); //输出结果: 'a' 'b'

是不是便捷很多了,代码又很清晰,所以Callbacks它是一个多用途的回调函数列表对象,提供了一种强大的方法来管理回调函数队列。

那么我们使用回调函数,总的来说弱化耦合,让调用者与被调用者分开,调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件的被调用函数。

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="http://img.mukewang.com/down/540812440001e40e00000000.js" type="text/javascript"></script>
<title></title>
</head>
<body> <script type="text/javascript"> function Aaron(List, callback) {
setTimeout(function() {
var task;
if (task = List.shift()) {
task(); //执行函数
}
if (List.length > 0) { //递归分解
arguments.callee(List)
} else {
callback()
}
}, 25)
} function show(data){
$("body").append('<li>'+ data +'</li>')
} Aaron([
function() {
show('a')
},
function() {
show('b')
},
function() {
show('c')
}
], function() {
show('callback')
}) var callbacks = $.Callbacks();
callbacks.add(function() {
show('callbacksA');
})
callbacks.add(function() {
show('callbacksB');
})
callbacks.fire(); </script> </body>
</html>

理解观察者模式

jQuery回调对象之前,我们有必要先理解其背后的设计思想 - “观察者模式”。

观察者模式 (pub/sub) 的背后,总的想法是在应用程序中增强松耦合性。并非是在其它对象的方法上的单个对象调用。一个对象作为特定任务或是另一对象的活动的观察者,并且在这个任务或活动发生时,通知观察者。观察者也被叫作订阅者(Subscriber),它指向被观察的对象,既被观察者(Publisher 或 subject)。当事件发生时,被观察者(Publisher)就会通知观察者(subscriber)。

观察者的使用场合就是:当一个对象的改变需要同时改变其它对象,并且它不知道具体有多少对象需要改变的时候,就应该考虑使用观察者模式。先看官网的demo这个例子,涉及到了 add 与 fire方法

是基于发布订阅(Publish/Subscribe)的观察者模式的设计。

作为 $.Callbacks() 的创建组件的一个演示,只使用回调函数列表,就可以实现 Pub/Sub 系统,将 $.Callbacks 作为一个队列。

我们来模拟常规下最简单的实现:

JS里对观察者模式的实现是通过回调来实现的,我们来先定义一个Observable对象,其内部包含了2个方法:订阅add方法与发布fire方法,如下代码:

var Observable = {
callbacks: [],
add: function(fn) {
this.callbacks.push(fn);
},
fire: function() {
this.callbacks.forEach(function(fn) {
fn();
})
}
}

使用add开始订阅:

Observable.add(function() {
alert(1)
}) Observable.add(function() {
alert(2)
})

使用fire开始发布:

Observable.fire(); // 1, 2

设计的原理:

开始构建一个存放回调的数组,如this.callbacks= [] 添加回调时,将回调push进this.callbacks,执行则遍历this.callbacks执行回调,也弹出1跟2了。当然这只是简洁的设计,便于理解,整体来说设计的思路代码都是挺简单的,那么我们从简单的设计深度挖掘下这种模式的优势。

注意:如果没有做过复杂交互设计,或者大型应用的开发者,可能一开始无法理解这模式的好处,就简单的设计而言用模式来处理问题,有点把简单的问题复杂化。我们不是为了使用模式而使用的。

组件开发为了保证组件可以在不同的项目中都适用,其必须是对其常用功能抽象出来加以实现,绝不会包含具体的业务逻辑而某一特定的项目使用者在其业务场景中使用组件时不可避免的要加入不同场景的业务逻辑。

实例代码 :

<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<script src="http://img.mukewang.com/down/540812440001e40e00000000.js" type="text/javascript"></script>
<title></title>
</head>
<body> <script type="text/javascript"> function show(data) {
$("body").append('<li>' + data + '</li>')
} var Observable={
callbacks:[],
add:function(fn){
this.callbacks.push(fn);
},
fire:function(){
this.callbacks.foEach(function(fn){ fn();
})
}
} var Observable = {
callbacks: [],
add: function(fn) {
this.callbacks.push(fn);
},
fire: function() {
this.callbacks.forEach(function(fn) {
fn();
})
}
} //使用add开始订阅:
Observable.add(function() {
show('你好上')
})
Observable.add(function() {
show('nihao 下')
}) //使用fire发布订阅的内容
Observable.fire(); </script> </body>
</html>