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版本兼容實驗

相關文章
相關標籤/搜索