分离构造器

通过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的更多相关文章

  1. jquery源码解读

    最近一直在研读 jQuery 源码,初看源码一头雾水毫无头绪,真正静下心来细看写的真是精妙,其结构明晰,高内聚.低耦合,兼具优秀的性能与便利的扩展性,在浏览器的兼容性(功能缺陷.渐进增强)优雅的处理能 ...

  2. jQuery源码解读三选择器

    直接上jQuery源码截取代码 // Map over jQuery in case of overwrite _jQuery = window.jQuery, // Map over the $ i ...

  3. jQuery源码解读 --- 整体架构

    最近学习比较忙,感觉想要提高还是要读源码,所以准备考试这个考试结束就开始读jquery源码啦,加油~

  4. jQuery源码解读-事件分析

    最原始的事件注册 addEventListener方法大家应该都很熟悉,它是Html元素注册事件最原始的方法.先看下addEventListener方法签名: element.addEventList ...

  5. jQuery源码解读 - 数据缓存系统:jQuery.data

    jQuery在1.2后引入jQuery.data(数据缓存系统),主要的作用是让一组自定义的数据可以DOM元素相关联——浅显的说:就是让一个对象和一组数据一对一的关联. 一组和Element相关的数据 ...

  6. jquery源码解读 (摘自jQuery源码分析系列图书(pdf)) 持续更新

    1.总体架构 1.1自调用匿名函数 //自调用匿名函数 (function(window,undefined){ //jquery code})(window); 1.这是一个自调用匿名函数.第一个括 ...

  7. jQuery源码解读一

    (function(window,undefined){...})(window); 这是一个典型的自执行的匿名函数. 为什么会有一个名为undefined的形参呢? undefined不是常量,可以 ...

  8. jQuery源码解读----part 1

    来源:慕课网 https://www.imooc.com/video/4392 jQuery整体架构 jQuery按我的理解分为五大块,选择器.DOM操作.事件.AJAX与动画, 那么为什么有13个模 ...

  9. (转)jQuery源码解读 -- jQuery v1.10.2

    原文GitHub链接: https://github.com/chokcoco/jQuery-

随机推荐

  1. vue路由(一)

    “vue.js 路由允许我们通过不同的 URL 访问不同的内容.通过 Vue.js 可以实现多视图的单页Web应用(single page web application,SPA)”这段是从网上直接抄 ...

  2. js 定时器(setTimeout/setInterval)出现变量未定义(xxx is not defined) 的解决方法

    首先声明本人资质尚浅,如有错误,欢迎指正.共同提高. ------------------------------------------------------------------------- ...

  3. WebApplication 启动类一定要存于某个包下

    否则spring启动器 无法识别 包类目录

  4. 中国大学MOOC课程信息之数据分析可视化一

    版权声明:本文为博主原创文章,转载 请注明出处:https://blog.csdn.net/sc2079/article/details/82263391 9月2日更:中国大学MOOC课程信息之数据分 ...

  5. split()函数实现

    #split函数实现: ss='** *axx* *bv** *ctt** **dff***' result=[] def split_1(ss,a,times=len(ss)): i=0 n=0 w ...

  6. APP微信登录 服务器处理代码

    采用框架THINKPHP5 需要客户端传的参数有  udid openid nickname avatar_path /* * @param 第三方微信登录 * @param openid udid ...

  7. 0003SpringBoot整合SpringDataJPA

    SpringBoot整合SpringDataJpa步骤如下: 1.添加data-jpa起步依赖(pom.xml) 2.添加数据库驱动坐标.添加Junit起步依赖(pom.xml) 3.添加数据库连接信 ...

  8. PhpStudy升级MySQL版本到5.7

    1:备份当前数据库数据. 最好是导成 SQL 文件 2:备份 PhpStudy 下的 MySQL 文件夹.以防升级失败.还可以使用旧版本的数据库 3:下载MySQL5.7.解压.然后放在 PhpStu ...

  9. Property or method "openPageOffice" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by

    Property or method "openPageOffice" is not defined on the instance but referenced during r ...

  10. django创建路径导航

    路径导航 :         1.怎样设置需要登录但又不需要验证权限的路径 :                 在settings中定义一个列表,列表中以正则的方式放入需要登录但无需验证的权限的项.在 ...