jQuery2.x源码解析(设计篇)

jQuery2.x源码解析(构建篇) css

jQuery2.x源码解析(设计篇) html

jQuery2.x源码解析(回调篇) node

jQuery2.x源码解析(缓存篇) jquery

 

这一篇笔者主要以设计的角度探索jQuery的源代码,不少人说jQuery设计过于我的主义话,其实这样说是有必定偏见的,由于好的设计是可通用的、共通的,jQuery这么好用,咱们怎么能说他的设计是我的主义呢?记得之前有人吐槽mvvm设计剑走偏锋,致使代码难以维护,不过前几年从mvvm火爆程度来看,另类毫不是很差。好了,开始正题。webpack


提问:jQuery是怎么暴露本身的api的?

任何框架其实都是个门面模式,外部与框架的通讯必须经过一个统一的门面,而这个门面就是咱们说所的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是如何建立在window上面的?

答: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支持在nodejs上运行吗?

既然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是如何对自身扩展的?

答: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.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({...})”对其扩展。


提问:jQuery是如何new出jQuery对象的?

看来艾伦的博客的评论,不少人在这里都没搞明白。尤为对它的原型和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对象本质是一个经过原型继承数组对象的方式得到的。可是咱们回到上一节的代码,咱们将以前的几段代码整理一下,能够获得

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”的相关文章。


提问:jQuery是如何实现链式操做?

答:很简答,就是“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的重载函数的?

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,猜想的含义分别为:

  • elems : 调用fn对自身操做的集合
  • fn : 须要封装的函数
  • key : 键值,若是value是undefined,表示当前是getter调用;或者是一个map,里面是key、value形式传递多个赋值项
  • value : 值,也能够是个函数(function(index, attr))
  • chaunable : true->setter调用;false->getter调用
  • emptyGet : elems为空的返回值
  • raw: true->key是字符串;false->key是函数

咱们先肯定何时函数封装的调用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是如何作版本控制的?

答:咱们知道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版本兼容实验

相关文章
相关标签/搜索