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