jQuery2.x源码解析(设计篇) html
jQuery2.x源码解析(回调篇) node
jQuery2.x源码解析(缓存篇) jquery
这一篇笔者主要以设计的角度探索jQuery的源代码,不少人说jQuery设计过于我的主义话,其实这样说是有必定偏见的,由于好的设计是可通用的、共通的,jQuery这么好用,咱们怎么能说他的设计是我的主义呢?记得之前有人吐槽mvvm设计剑走偏锋,致使代码难以维护,不过前几年从mvvm火爆程度来看,另类毫不是很差。好了,开始正题。webpack
任何框架其实都是个门面模式,外部与框架的通讯必须经过一个统一的门面,而这个门面就是咱们说所的api。所以学习任何框架的源码,咱们都要弄清两件事:git
1.哪些是私有方法,由于私有方法是框架本身内部使用,是他不但愿暴露给外围用户的,这些方法是不能做为api,即使用户能够看到他们。github
2.哪些方法是api,他们是真正暴露给用户使用的。这些方法的定义每每面向接口,相对稳定,不会由于框架内部修改而改变。只有这样,框架的使用者才不会由于升级框架而修改他们自身的代码,符合“开闭原则”和“里氏替换原则”。web
那么jQuery是怎么实现门面模式,暴露本身的api呢?npm
答: jQuery是建立在window上面的,并且在window上仅建立两个变量,一个是“$”,一个是“jQuery”,而且两者指向同一个对象——jQuery函数。设计模式
window.jQuery = window.$ = jQuery;
jQuery为何要暴露两个同样的变量名呢?主要是jQuery是六个字符,打起来比较麻烦,因此就用一个字符的别名“$”来替代,这样使用者能够少打五个字符-_-||。不少框架也是暴露两个对象,好比underscore、lodash的_。
jQuery自己是一个函数(简称$函数),经过调用这个函数咱们能够返回一个对象,咱们称为jQuery对象,jQuery对象的原型是jQuery.fn.init,在这原型上jQuery提供了不少方法供使用者使用。$虽然是个函数,可是函数也是能够有其成员变量的,因此$自身的成员变量咱们也是能够利用的。
所以jQuery提供了三种api:
一个是jQuery自己,也就是$函数,它是一个函数,同时也是一个api,能够建立jQuery对象。
另外一个jQuery对象上的api,jQuery经过扩展原型(jQuery.fn)的形式,提供列jQuery对象上的种种成员方法,供用户使用。
最后是JQuery函数上面的成员方法,这些方法一样能够做为全局方法、util方法来使用。
而且jQuery并未注明私有(由于js自身语法的限制,因此不少私有成员在外部仍是能看到,对于这种私有成员,咱们会建立一个命名规则加以区分,如“$”、“_”、“$$”开头等),全部暴露的方法所有是api。
答:jQuery的主要构建模式为先用一个IIFE将自身扩展起来,这样的好处是不污染全局做用域。同时使用了严格模式"use strict",严格模式的声明必须放到IIFE里面,一样是为了避免污染全局,毕竟jQuery不可能让本身严格模式必须在严格模式下才能运行。
jQuery正真的构造方法是经过做为IIFE块的参数的形式,传进去IIFE块里面的,在IIFE里面视状况调用这个构造方法。
首先jQuery支持commonjs,能够直接require(‘jquery.js’)将jQuery引入。须要注意的是,在commonjs环境下,若是全局做用域支持document对象,就建立在全局做用域上,若是不支持就返回一个新的工厂函数,使用者在须要的时候经过这个新的构造函数,去建立jQuery,同时还需将document传递进入。jQuery本就是给浏览器中使用的,因此即便支持commonjs,可是运行时候仍是离不开浏览器环境。
//使用IIFE,将jQuery建立的整个过程封装到一个闭包里,而后将全局变量(若是是浏览器环境就是window,若是是commonjs环境就是当前做用域)和工厂函数传入进去 (function( global, factory ) { //严格模式在闭包中,一样不会对全局做用域产生污染 "use strict"; //这里面是判断是不是commonjs环境,若是是就用commonjs把jQuery的构造结果输出去。若是不是就用全局变量构建jQuery if ( typeof module === "object" && typeof module.exports === "object" ) { module.exports = global.document ? factory( global, true ) : function( w ) { if ( !w.document ) { throw new Error( "jQuery requires a window with a document" ); } return factory( w ); }; } else { factory( global ); } //根据有没有window判断是不是浏览器环境 })( typeof window !== "undefined" ? window : this, function( window, noGlobal ) { //正在的构建过程 var jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ); } //若是用commonjs输出就不在window上面构建jQuery了,而是直接以返回值输出 if ( !noGlobal ) { window.jQuery = window.$ = jQuery; } return jQuery; });
这个建立过程和webpack的umd模块的建立过程很像,umd是同时支持amd、commonjs、web的script调用的一种模块化方式,jQuery不支持amd模块,可是同时支持commonjs和web,构造形式也有umd大致同样,能够算一个简化的umd模块。
既然jQuery支持commonjs,那么他能够在node里面运行吗?
答:咱们在npm运行
npm install jquery
确实安装了jQuery,可是使用的时候须要用一个存在document的对象对其初始化。此时咱们须要jsDom,这个能够在node跑DOM的库。
安装jsDOm
npm install jsdom
而后在node执行
var $ = require("jquery"); var jsdom = require("jsdom"); jsdom.env( "<div id='div'>hello world</div>", function (err, window) { $ = $(window); console.log($("#div").html()) } );
打印出“hello world”,咱们获得了想要的结果。
不过,jQuery彻底依赖于浏览器模型,须要jsDom这样的库作支持,为了运行jQuery去模拟这样一个模型有些小题大作的感受。笔者以前使用过另外一个在node端仿jQuery项目——cheerio,cheerio的api很jQuery很像,熟悉jQuery的朋友能够很快上手,咱们可使用这个来处理node中的dom操做,这对于抓包抽取数据等工做很是适合。总之jQuery是为浏览器设计的,在非浏览器环境下尽可能不要考虑使用,由于确定有更好的替代品。
除了这些,npm上面还有一个jQuery的库,名字就叫jQuery(浓浓的山寨味道),笔者曾经觉得这是正统的jQuery而误装过这个库。
npm install jQuery
这个库与jquery仅仅是一个大小写之差,却彻底是两个东西,安装的时候必定要注意。
答:艾伦将$函数视为反模式设计,这是由于$是jQuery的惟一入口,而且强行将几种不一样的功能重载为一个功能。这样的好处是很明显的,简化了对外的api,使得整个jQuery的api更加的简洁,学习起来更加简单快捷。jQuery整个框架都是以快速简洁为目的,这个设计很符合他自身的设计需求。
可是这样的设计是反模式的,主要是和“职责单一原则”冲突,强行将几种彻底不一样的功能重载在一块儿,很不利于使用者对其的理解。重载函数是指相同功能可是参数不一样的几个函数的同名策略。由于这些函数功能相同,同名更有利于你们学习与维护。不一样功能的函数重载在一块儿是不可取的,这是不符合设计模式的。
不过适当的反模式,换来的是api的简洁与使用,这是有利于用户学习与使用的。
具体以下:
首先$函数就是new了一个$.fn.init对象:
var jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ); }
这个jQuery.fn.init方法的具体作了什么?笔者总结,共4中功能:
1.经过jQuery选择器选择dom,并将其封装为jQuery对象返回
2.将html字符串、DOM对象生成DOM碎片,并将其封装为jQuery对象返回
3.对于domcontentloaded事件的封装与实现
4.将任意对象封装为jQuery对象
答:jQuery中最核心的函数是$.extend,他实现相似ES6的Object.assign函数,他的最终目的是实现Mixin设计模式。
Mixin模式,也叫织入模式。就是一些提供可以被一个或者一组子类简单继承功能的类,意在重用其功能。与传统继承的思想不一样,Mixin是经过扩展对象的方法实现的,这样的好处就是,能够先建立对象,而后再对其扩展。这个设计模式是JavaScript中最重要设计模式之一,他充分利用了JavaScript的可以对对象动态扩展的功能,可以实现原型模式等、继承等功能。
$.extend函数的核心目的就是对Mixin模式的实现,固然$.extend的功能不仅如此,还能够作克隆对象、深拷贝、替代Object.assign等功能。不过为自身扩展才是这个函数最核心的功能,咱们想来看看jQuery对象的建立过程。
jQuery自己就是一个函数,在其建立以后,又为本身建立了一个基础的原型fn。
jQuery.fn = jQuery.prototype = { // 很是少的几个方法 ... }
而后又在自身和自身原型上定义了extend函数。
jQuery.extend = jQuery.fn.extend = function() { ... }
接着使用extend扩展自身的及其原型上的功能。
jQuery.extend( {
...
})
jQuery.fn.extend( {
...
})
整个jQuery的建立过程就是使用Mixin模式对自身不断地扩展功能。同时由于Mixin模式的扩展是建立对象后才进行的,因此咱们没必要担忧扩展功能时候去修改先前的代码,更加体现“开闭原则”。
同时,使用extend扩展jQuery的功能是官方推荐的,jQuery自身代码就是使用这种方式,所以咱们扩展jQuery的时候,尽可能不该使用“$.fn.xxx = ”这种语句,而是应该使用jQuery为咱们暴露的api——“$.fn.extends(...)”,这样才是最标准的用法,尽可能不要使用“$.fn.xxx = ...”的形式。只有这样,咱们的代码才不会担忧将来由于jQuery版本升级,而带来的兼容性问题。
jQuery.fn = jQuery.prototype = { ... }
从上面代码能够看出,jQuery的fn就是JavaScript语法原型prototype,为何要换一个名字呢?
答:浅显而说,仍是为了简练,利于压缩,由于fn比prototype少了7个字符-_-,可是笔者认为这里还有更深层的含义。
仍是回到门面模式上,prototype是JavaScript语法层面上的,是属于jQuery的私有的部分,不但愿用户修改,同时jQuery还但愿把自身原型暴露出去,所以须要对其进行封装,这个封装哪怕仅仅是改一个名字。咱们能够想象一下,若是将来jQuery对其自身的api结构进行修改,再也不直接使用prototype这个js提供的原型,那么他对外提供的api是能够作到不修改的,由于他暴露的是fn而不是prototype。固然这种修改的可能性是微乎其微的,可是jQuery的做者仍是将其考虑进去了,这体现了其做者扎实的基本功,对设计模式和设计原则有着深入的理解,这是咱们应该学习的。
这就是为何JavaScript存在prototype这个语法,可是jQuery偏不直接使用,而是将其重命名为fn的缘由。所以咱们在写jQuery的原型扩展的时候,要尽可能使用“$.fn.extends({...})”的语句,而不要使用“$.prototype.extends({...})”对其扩展。
看来艾伦的博客的评论,不少人在这里都没搞明白。尤为对它的原型和this的处理没搞明白。
答:咱们分析过jQuery的$函数的几个功能,其中大多数功能都是封装jQuery对象。其实$函数自己就是一个工厂函数,jQuery对象就是经过这个工厂函数封装的方法建立出来的。这个过程很精妙,咱们以前也说过,真正的jQuery对象的原型是jQuery.fn.inti。
init = jQuery.fn.init = function( selector, context, root ) {...}
init.prototype = jQuery.fn;
从上面的代码我能够看出,init的原型等于jQuery的原型。
为何要这么作呢?jQuery使用$()代替new $(),这样一会儿少了4个字符-_-,同时有也符合工厂模式,毕竟直接使用语法级的new是不符合工厂模式的。同时将jQuery的原型,赋给jQuery.fn.init的原型。这样设计的目的并不只仅是为了省几个字符,更重要的是jQuery.fn.init的原型也是jQuery的api的一部分,事实上jQuery的原型自己并非咱们的api,由于jQuery对象的原型是jQuery.fn.init对象,而并不是是jQuery。可是以jQuery的原型做为api,更利于用户理解与使用。
所以才会有:
jQuery.fn.init.prototype = jQuery.fn;
这句代码的含义是使用jQuery.fn代替jQuery.fn.init.prototype做为jQuery对外暴露的jQuery对象的原型的接口,暴露给用户。所以咱们对jQuery.fn的扩展,天然也会扩展到jQuery.fn.init的对象上面,由于jQuery.fn.init.prototype就是jQuery.fn,而jQuery对象的原型是jQuery.fn.init对象,所以天然也会扩展到jQuery对象上面。
那么jQuery为何要建立一个jQuery.fn.init来做为jQuery对象的原型,而不直接在jQuery函数里面new自身呢?
这一点艾伦的博客已经给出了解释,直接在构造方法里面new方法建立自身,会陷入死循环。而jQuery设计的漂亮之处,就在于定义了jQuery.fn.init做为jQuery对象的原型,同时这个这个对于用户而言又是透明的,用户无需知道他的存在,也无需知道jQuery.fn.init.prototype的存在。这样暴露出去的api是最简洁的api,利于你们使用。
艾伦的博客更可能是从语法层面解释的,而笔者更多的是从设计角度考虑的,jQuery之因此这么作,其目的是为了追求对外暴露最简洁的api。所以jQuery内部才会设计的如此复杂与精妙。
答:曾经笔者一直觉得,jQuery对象本质是一个经过原型继承数组对象的方式得到的。可是咱们回到上一节的代码,咱们将以前的几段代码整理一下,能够获得
jQuery.fn.init.prototype = JQuery.fn = jQuery.prototype = {...};
能够看出jQuery对象就是一个普通对象,不该该说是“Array-like Object”(简称ArrayLike对象)。由于jQuery自己是具有length,其实就是仿造数组,定义了一个带索引和length的普通对象。这种对象咱们能够说是“Array-like Object”对象。
jQuery.fn = jQuery.prototype = { ... length: 0, }
由于jQuery的原型上定义了length=0,至关于一个空的“Array-like Object”。
咱们能够看看jQuery.fn.init构造方法
init = jQuery.fn.init = function( selector, context, root ) { if ( !selector ) { return this; } ... if ( typeof selector === "string" ) { if(...){ jQuery.merge( this, jQuery.parseHTML( match[ 1 ], context && context.nodeType ? context.ownerDocument || context : document, true ) ); return this; } else if(...){ elem = document.getElementById( match[ 2 ] ); if ( elem ) { this[ 0 ] = elem; this.length = 1; } return this; } ... } else if (...) { this[ 0 ] = selector; this.length = 1; return this; } else if (...) { return ... } else... return jQuery.makeArray( selector, this ); };
方法在return前,调用了jQuery.makeArray函数、jQuery.merge函数,或者是经过“[]”和“length”来为this扩展,这些都是对ArrayLike对象的处理函数,由于this是拥有jQuery.fn原型的对象,所以这里的this是一个ArrayLike对象,而通过jQuery.makeArray、jQuery.merge等处理过的this还是一个ArrayLike对象,因此最终返回的就是一个ArrayLike对象。
最后,jQuery经过内部的jQuery.uniqueSort确保其集合中不会出现重复的元素,因此jQuery对象不可是一个ArrayLikeObject集合,同时集合里面的元素是不重复的。
此外,jQuery还提供了一是判断对象是不是ArrayLikeObject的函数。若是对象是ArrayLike对象,jQuery还提供了诸多处理集合运算的相关函数,如get、filter、each、merge等函数。这些函数本都是数组函数,可是ArrayLike对象实际上都是适用的,事实上不少数组方法,均可以给ArrayLike对象使用,有兴趣的能够查一查“Array-like Object”的相关文章。
答:很简答,就是“return this”。同时对于集合操做,可使用jQuery.each。
jQuery.each设计的很是巧妙,由于他自己也会返回自身:
jQuery.extends({ each:function(obj, callback){ ... return obj; } }); jQuery.fn.extends({ each: function( callback ) { return jQuery.each( this, callback ); }, });
经过each,咱们能够很容易的将不少集合运算包装为支持链式操做的形式。
toggle: function( state ) { if ( typeof state === "boolean" ) { return state ? this.show() : this.hide(); } return this.each( function() { if ( isHidden( this ) ) { jQuery( this ).show(); } else { jQuery( this ).hide(); } } ); }
使用这种形式,一个集合操做函数能够被很是容易的包装支持成链式操做。
咱们写jQuery插件,不少时候都须要支持jQuery的连接操做功能,使用each来封装咱们本身的插件是很好的选择。
同时,jQuery的集合操做函数,也是支持链式操做的,jQuery的集合操做,都会把以前的集合缓存起来,咱们能够经过prevObject和end方法得到集合运算前的集合,这样的操做大大增长列链式操做的适用场景。
其余支持链式操做的api有$.Deferred、jQuery的动画操做等,这里暂不展开。
jQuery有个特色,就是不少函数重载的setter和getter方法,同时他们还支持JSON形式的key、value赋值、链式调用等功能,这样的函数有attr、prop、text、html、css、data等,他们是如何封装的?
答:秘密就在access.js,以上函数都调用了这个私有函数进行封装的。
首先须要他们提供一个重载函数:
fn(elem, key)和fn(elem, key,value)
前一个是elem的getter函数,后一个是elem的setter函数。接下来经过access来对fn进行封装,使其可以支持集合操做、JSON形式的key、value赋值、链式操做等功能。
access的入参有elems, fn, key, value, chainable, emptyGet, raw,猜想的含义分别为:
咱们先肯定何时函数封装的调用getter,何时调用setter。当key是对象,或者value不为undefined的时候,是对setter的调用;不然就是getter调用。
先看getter:
若是key是空(包括undefined、null,不包括0、空字符),会执行
fn.call( elems )
这也是一个重载方法,能够用于对如sum、avg等函数的封装,经过整个elems计算一个值返回。
若是key不是null,则取elems第一个参数的key对应的值;若是elems为空数组,则返回emptyGet。
再看setter:
和getter同样,setter一样是分为key是空和不是空两种状况。
在key是空的状况下,会对整个集合作操做。
若是key是一个JSON,会遍历这个JSON的key,依次递归调用access进行循环赋值。
不然key既不是空,也不是JSON,会用key作key值,依次对elems里面的元素赋值。同时value能够是数组,此时会经过当前elem、elem在elems的位置index、elem的key对应的当前值做为参数,调用value函数,计算最终的value赋值给elem。
access自己是个模板模式,经过access,将fn进行了扩展,这体现了函数式的函数柯里化思想,轻松地建立了众多重载函数,并简化了封装过程。采用柯里化化思想实现模板模式,也体现了JavaScript这门语言的灵活之处。
答:咱们知道jQuery是要向window占用两个变量名,“$”和“jQuery”,$是别名,而jQuery是真正的名字,因此jQuery在建立的时候,把window上原有的“$”和“jQuery”变量保存起来,而后在建立自身。
而且提供了将保存“$”和“jQuery”变量原有的功能noConflict:
var _jQuery = window.jQuery, _$ = window.$; jQuery.noConflict = function( deep ) { if ( window.$ === jQuery ) { window.$ = _$; } if ( deep && window.jQuery === jQuery ) { window.jQuery = _jQuery; } return jQuery; };
不少库也是这么作版本控制的,如underscore。
关于版本更多信息能够参考笔者之前的博客jQuery版本兼容实验。