jQuery源码解读----part 2
分离构造器
通过new操作符构建一个对象,一般经过四步:
A.创建一个新对象
B.将构造函数的作用域赋给新对象(所以this就指向了这个新对象)
C.执行构造函数中的代码
D.返回这个新对象
最后一点就说明了,我们只要返回一个新对象即可。其实new操作符主要是把原型链跟实例的this关联起来,这才是最关键的一点,所以我们如果需要原型链就必须要new操作符来进行处理。否则this则变成window对象了。
改造jQuery无new的格式,我们可以通过instanceof判断this是否为当前实例:
var $$ = ajQuery = function(selector) {
if(!(this instanceof ajQuery)){ // 第二次看还是觉得这一句很NB
return new ajQuery(selector);
}
this.selector = selector;
return this
}
但在jQuery实际上采取的手段是把原型上的一个init方法作为构造器,这样貌似更节省代码空间?
var $$ = ajQuery = function(selector) {
//把原型上的init作为构造器
return new ajQuery.fn.init( selector );
}
ajQuery.fn = ajQuery.prototype = {
name: 'aaron',
init: function() {
console.log(this)
},
constructor: ajQuery
}
但这样子还缺点东西,init是ajQuery原型上作为构造器的一个方法,那么其this就不是ajQuery了,所以this就完全引用不到ajQuery的原型了,所以这里通过new把init方法与ajQuery给分离成2个独立的构造器。
静态与实例方法共享设计
接着上面分割出2个构造器的疑问,来看看jQuery的一个遍历接口:
$(".aaron").each() //作为实例方法存在
$.each() //作为静态方法存在
看似实例和静态方法需要两个函数来实现,但在jQuery源码中是这样的:
jQuery.prototype = {
// 调用实例方法实际上是将实例对象this作为一个参数,调用对应的静态方法,这样就形成了共享
each: function( callback, args ) {
return jQuery.each( this, callback, args );
}
}
实例方法取于静态方法,换句话来说这是静态与实例方法共享设计,静态方法挂在jQuery构造器上,原型方法挂在哪里呢?------jQuery通过new原型prototype上的init方法当作构造器,那么init的原型链方法就是实例的方法了,所以jQuery通过2个构造器划分2种不同的调用方式一种是静态,一种是原型。
那如果要将2个构造器原型关联起来,关键就是靠下面一句:
ajQuery.fn.init.prototype = ajQuery.fn
这样init构造出来的实例对象也能够继承jQuery原型上的方法了。
方法链式调用的实现
jQuery的核心理念是Write less,Do more(写的更少,做的更多),那么链式方法的设计与这个核心理念不谋而合。那么从深层次考虑这种设计其实就是一种Internal DSL。
DSL是指Domain Specific Language,也就是用于描述和解决特定领域问题的语言。
jQuery的Internal DSL形式带来的好处——编写代码时,让代码更贴近作者的思维模式;阅读代码时,让读者更容易理解代码的含义;应用DSL可以有效的提高系统的可维护性(缩小了实现模型和领域模型的距离,提高了实现的可读性)和灵活性,并且提供开发的效率。
jQuery的这种管道风格的DSL链式代码,总的来说:
☑ 节约JS代码;
☑ 所返回的都是同一个对象,可以提高代码的效率
实现链式操作的原理大家都懂的,就只需要在方法内返回当前的这个实例对象this就可以了,因为返回当前实例的this,从而又可以访问自己的原型了,这样的就节省代码量,提高代码的效率,代码看起来更优雅。但是这种方法有一个问题是:所有对象的方法返回的都是对象本身,也就是说没有返回值,所以这种方法不一定在任何环境下都适合。
插件接口的设计
jQuery插件的开发分为两种:
☑ 一种是挂在jQuery命名空间下的全局函数,也可称为静态方法;
☑ 另一种是jQuery对象级别的方法,即挂在jQuery原型下的方法,这样通过选择器获取的jQuery对象实例也能共享该方法。
提供的接口:
$.extend(target, [object1], [objectN]);
$.fn.extend();
接口的使用:
// 拓展到jQuery上的静态方法
jQuery.extend({
data:function(){},
removeData:function(){}
})
// 拓展到实例对象上的原型方法
jQuery.fn.extend({
data:function(){},
removeData:function(){}
})
而jQuery源码中对于上面两种扩展,其实是同指向同一方法的不同引用(这里有一个设计的重点,通过调用的上下文,我们来确定这个方法是作为静态还是实例处理,在javascript的世界中一共有四种上下文调用方式:方法调用模式、函数调用模式、构造器调用模式、apply调用模式),而这一切都是依靠this来完成的。
☑ jQuery.extend调用的时候上下文指向的是jQuery构造器,this指向的是jQuery ☑ jQuery.fn.extend调用的时候上下文指向的是jQuery构造器的实例对象了,this指向实例对象
因此在源码中是这样的:
aAron.extend = aAron.fn.extend = function() {
var options, src, copy,
target = arguments[0] || {},
i = 1,
length = arguments.length;
// 只有一个参数,就是对jQuery自身的扩展处理
if (i === length) {
target = this; // 调用的上下文对象,前一个方法对应jQuery,后一个方法对应实例
i--;
}
for (; i < length; i++) {
// 从i开始取参数,不为空开始遍历
if ((options = arguments[i]) != null) {
for (name in options) {
copy = options[name];
// 覆盖拷贝
target[name] = copy;
}
}
}
return target;
}
我来讲解一下上面的代码:因为extend的核心功能就是通过扩展收集功能(类似于mix混入),所以就会存在收集对象(target)与被收集的数据,因为jQuery.extend并没有明确实参,而且是通过arguments来判断的,所以这样处理起来很灵活。arguments通过判断传递参数的数量可以实现函数重载。其中最重要的一段target = this,通过调用的方式我们就能确实当前的this的指向,所以这时候就能确定target了。最后就很简单了,通过for循环遍历把数据附加到这个target上了。当然在这个附加的过程中我们还可以做数据过滤、深拷贝等一系列的操作了。
回溯处理的设计
通过jQuery处理后返回的不仅仅只有DOM对象,而是一个包装容器,返回jQuery对象。而这一个对象中有一个preObject的属性。
要了解这个属性是做什么的,首先了解一下jQuery对象栈,jQuery内部维护着一个jQuery对象栈。每个遍历方法(在当前选中范围内的DOM再进行筛选的操作,例如.find()方法)都会找到一组新元素(一个jQuery对象),然后jQuery会把这组元素推入到栈中。
而每个jQuery对象都有三个属性:context、selector和prevObject(用id选择器的话这个属性不一定有),其中的prevObject属性就指向这个对象栈中的前一个对象,而通过这个属性可以回溯到最初的DOM元素集中。
可以看下下面的例子:
$("div").find('.foo').find('.aaa') // 这里的preObject属性就会指向$("div").find('.foo')的DOM集合
$("div").find('.foo') // 往前一级的preObect属性就是指向$("div")的DOM集合
而这种可以回溯到之前选择的DOM集合的机制,是为这两个方法服务的:
.end() // 回溯到前一个jQuery对象,即prevObject属性
.addBack() // 把当前位置和前一个位置的元素结合组合起来,并且将这个新的组合的元素集推入栈的上方
而利用这个回溯机制和对应的方法可以进行如下的操作:
<ul class="first">
<li class="foo">list item 1</li>
<li>list item 2</li>
<li class="bar">list item 3</li>
</ul>
<script>
// foo类li标签背景设置为红色, bar类li标签背景设置为绿色
$("#test2").click(function(){
//通过end连贯处理
$('ul.first')
.find('.foo')
.css('background-color', 'red')
.end()
.find('.bar')
.css('background-color', 'green');
})
</scripts>
利用这个DOM元素栈可以减少重复的查询和遍历的操作,而减少重复操作也正是优化jQuery代码性能的关键所在。
end
end方法能够帮助我们回溯到上一个DOM合集,因此该方法返回的就是一个jQuery对象,在源码中的表现就是返回了prevObject对象:
end: function() {
return this.prevObject || this.constructor(null);
}
那么prevObject在什么情况下会产生?
在构建jQuery对象的时候,通过pushStack方法构建,如下代码:
pushStack: function( elems ) {
// Build a new jQuery matched element set
// 这里将传进来的DOM元素,通过调用jQuery的方法构建成一个新的jQuery对象
var ret = jQuery.merge( this.constructor(), elems );
// Add the old object onto the stack (as a reference)
// 在此对象上把前一个jQuery对象添加到prevObject属性中
ret.prevObject = this;
ret.context = this.context;
// Return the newly-formed element set
// 最后返回这个jQuery对象
return ret;
}
那么在find方法中,为了将前一个jQuery对象推入栈中,就会调用这个pushStack方法来构建:
jQuery.fn.extend({
find: function(selector) {
//...........................省略................................
//通过sizzle选择器,返回结果集
jQuery.find(selector, self[i], ret);
// Needed because $( selector, context ) becomes $( context ).find( selector )
ret = this.pushStack(len > 1 ? jQuery.unique(ret) : ret); // 这里就是将实例对象推入栈中,然后返回新的jQuery对象
ret.selector = this.selector ? this.selector + " " + selector : selector;
return ret;
}
}
仿栈与队列的操作
jQuery既然是模仿的数组结构,那么肯定会实现一套类数组的处理方法,比如常见的栈与队列操作push、pop、shift、unshift、求和、遍历循环each、排序及筛选等一系的扩展方法。
jQuery提供了.get()、:index()、 :lt()、:gt()、:even()及 :odd()这类索引值相关的选择器,他们的作用可以过滤他们前面的匹配表达式的集合元素,筛选的依据就是这个元素在原先匹配集合中的顺序。
先来看看get方法的实现源码:
get: function(num) {
return num != null ? // 不传参为undefined, 走false线
// Return just the one element from the set
(num < 0 ? this[num + this.length] : this[num]) :
// Return all the elements in a clean array
slice.call(this); // 返回整个DOM元素数组
}
get与eq的区别
熟悉jQuery的童鞋都清楚,get返回的是DOM元素,而eq返回的是jQuery对象,这样就可以继续执行链式操作。
eq实现的原理:
eq: function( i ) {
var len = this.length,
j = +i + ( i < 0 ? len : 0 );
return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] );
上面实现代码的逻辑就是跟get是一样的,区别就是通过了pushStack产生了一个新的jQuery对象。
如果需要的是一个合集对象要怎么处理?因此jQuery便提供了一个slice方法,根据下标范围取元素集合,并生成一个新的jQuery对象。
slice方法实现源码:
slice: function() {
return this.pushStack( slice.apply( this, arguments ) );
},
迭代器
迭代器是一个框架的重要设计。我们经常需要提供一种方法顺序用来处理聚合对象中各个元素,而又不暴露该对象的内部,这也是设计模式中的迭代器模式(Iterator)。
针对迭代器,这里有几个特点:
☑ 访问一个聚合对象的内容而无需暴露它的内部。
☑ 为遍历不同的集合结构提供一个统一的接口,从而支持同样的算法在不同的集合结构上进行操作。
☑ 遍历的同时更改迭代器所在的集合结构可能会导致问题。
另外还要考虑这四点:
☑ 聚合对象,可能是对象,字符串或者数组等类型
☑ 支持参数传递
☑ 支持上下文的传递
☑ 支持循环中退出(返回false的时候退出循环,节省性能)
简单实现一个迭代器:
function each(obj, callback, context, arg) {
var i = 0;
var value;
var length = obj.length;
for (; i < length; i++) {
value = callback.call(context || null, obj[i], arg);
if (value === false) {
break;
}
}
jQuery的each迭代器
$.each()函数和$(selector).each()是不一样的,后者是专门用来遍历一个jQuery对象的,是为jQuery内部服务的。
jQuery的实例方法最终也是调用的静态方法,我们在之前就解释过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源码解读----part 2的更多相关文章
- jquery源码解读
最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐进增强)优雅的处理能 ...
- jQuery源码解读三选择器
直接上jQuery源码截取代码 // Map over jQuery in case of overwrite _jQuery = window.jQuery, // Map over the $ i ...
- jQuery源码解读 --- 整体架构
最近学习比较忙,感觉想要提高还是要读源码,所以准备考试这个考试结束就开始读jquery源码啦,加油~
- jQuery源码解读-事件分析
最原始的事件注册 addEventListener方法大家应该都很熟悉,它是Html元素注册事件最原始的方法.先看下addEventListener方法签名: element.addEventList ...
- jQuery源码解读 - 数据缓存系统:jQuery.data
jQuery在1.2后引入jQuery.data(数据缓存系统),主要的作用是让一组自定义的数据可以DOM元素相关联——浅显的说:就是让一个对象和一组数据一对一的关联. 一组和Element相关的数据 ...
- jquery源码解读 (摘自jQuery源码分析系列图书(pdf)) 持续更新
1.总体架构 1.1自调用匿名函数 //自调用匿名函数 (function(window,undefined){ //jquery code})(window); 1.这是一个自调用匿名函数.第一个括 ...
- jQuery源码解读一
(function(window,undefined){...})(window); 这是一个典型的自执行的匿名函数. 为什么会有一个名为undefined的形参呢? undefined不是常量,可以 ...
- jQuery源码解读----part 1
来源:慕课网 https://www.imooc.com/video/4392 jQuery整体架构 jQuery按我的理解分为五大块,选择器.DOM操作.事件.AJAX与动画, 那么为什么有13个模 ...
- (转)jQuery源码解读 -- jQuery v1.10.2
原文GitHub链接: https://github.com/chokcoco/jQuery-
随机推荐
- SAP UI5应用入口App.controller.js是如何被UI5框架加载的?
首先在UI5应用的manifes.json里,定义了UI5应用的入口视图为App: 调试器里的pending数组的两个元素: 实际上对应了我在App.controller.js里定义的两个依赖: 而a ...
- js form表单提交后如何可以不刷新页面 的解决办法
表单可实现无刷新页面提交,无需页面跳转,如下: 通过一个隐藏的iframe实现, form表单的target设置为iframe的name名称,form提交目标位当前页面iframe则不会刷新页面 &l ...
- 雨后清风U盘启动盘的五大用处及制作方法
如果有一个U盘可以帮助你安装系统,或者在你的电脑系统崩溃时帮助你修复系统,是不是很方便呢?雨后清风U盘启动盘就能帮你实现这样的效果.除此之外,雨后清风U盘启动盘还有另外一些用处.下面就来和大家分享一下 ...
- Java学习第二天之Java程序的基本规则
一.Java程序的组织形式 Java程序是一种纯粹的面向对象的程序设计语言,因此Java程序必须以类(即class)的形式存在,类(class)是Java程序的最小程序单位.Java程序不允许可执行性 ...
- 大数据之路week05--day01(I/O流阶段一 之File)
众所周知,我们电脑中有许许多多的文件夹和文件,文件的形式也有许多不同的格式,文件夹中也可以新建文件夹的存在,也就是多层的一步一步的嵌套. 我们想要实现I/O操作,就必须知道硬盘上文件的表现形式. 而J ...
- sublimetext插件自定义respository
官方文档(https://packagecontrol.io/docs/submitting_a_package) 上面说明了插件可以按两种方式挂起,一种是github,一种是ssl认证的web服务器 ...
- C#中的@符号的使用
一 字符串中的用法 字符@表示,其后的字符串是个“逐字字符串”(verbatim string). @只能对字符串常量作用. 1.用于文件路径 string s_FilePath ="C:\ ...
- C语言学习系列(二)面向过程和面向对象
一.基本定义 (一).面向过程(procedure oriented programming POP) 面向过程是分析解决问题的步骤,然后用函数把这些步骤一步一步的实现,然后在使用的时候一一调用则可. ...
- 浏览器顶部设置margin-top时存在的bug
浏览器bug<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8 ...
- Python3发送webservice请求
Python3使用suds-jurko库来发送webservice接口请求 导入请求webservice接口需要用到的包 pip install suds-jurko 第一步:导入所需要的包 from ...