原本想學習一下jQuery的源碼,但因爲jQuery的源碼有10000多行,設計至關複雜,因此決定從zepto開始,分析一個成熟的框架的代碼結構及執行步驟。javascript
網上也有不少zepto的源碼分析,有的給源碼添加註釋,有的談與jQuery的不一樣,可是都沒有系統的講解zepto框架的代碼結構及初始化Zepto對象的過程。css
默認你已經對面向對象有必定的瞭解,本文是邊實踐邊寫的,雖有些亂,但好處是爲你們提供了分析的思路。html
注意在文中$
變量表示一個函數對象,而$()
表示執行函數,他會返回另外一個對象。node
$
在文檔中能夠看到有兩類方法,其中一類是沒有$
前綴,例如addClass
,這些方法都有一個共同的特色,操做DOM或BOM。還有一類是有前綴的例如$.trim
,這一類方法無關平臺,只是封裝了一些經常使用的方法,能夠看做ECMA層級的方法,與瀏覽器無關。正則表達式
咱們分別打印,看如下log日誌segmentfault
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p id="person"> <span></span> </p> </body> </html> <!-- 以後的代碼中也使用這個html文檔 -->
console.log($.prototype); console.log($("#person")); console.log($);
結果如上圖,展開綠色1便可看到全部前綴的方法,展開圖中2可看到全部的不帶前綴的方法。圖中3返回的是一個函數。api
1中的結果能夠看出像$.trim
這類方法保存在$.prototype
的構造函數中,也就是在$
中,可是$
打印出來的是卻一個函數,爲了解決這中迷惑性,如下代碼重現了這種狀況,能夠看出,$
確實是一個函數,只是這個函數多了一些特定的方法。$.trim
只是$
的一個屬性。數組
2中的方法都在對象的原型函數中,由於它執行了$()
函數返回了一個對象Z
,該對象的原型中包含一些相似於addClass
方法。瀏覽器
var good = (function() { var g; var log = function(text){ console.log(text); } g = function(){console.log("666");} g.log = log; return g; })(); console.log(good.log("Are you OK?"));// Are you OK? console.log(good);// function(){console.log("666");}
寫到這裏忽然想起來console對象還有個dir方法,console.dir($)
清晰明瞭。-_-||
補充一張動圖
我認爲這種寫法的好處有,調用$()
後返回的對象是一個新對象,就沒有相似$.trim
這類方法,且addClass
這類方法都在原型函數中,更能節省內存。
不執行$
函數對象,只是調用其函數中的特定屬性,該對象只會建立一次(在引入zepto時就已經初始化了),一樣不會浪費內存。
兩種類型的方法共用同一個變量名,減小命名衝突的可能,封裝更完全。
下面開始自上而下的分析源碼,層層剝離,使脈絡清晰。
var Zepto = (function() { return $ })() window.Zepto = Zepto window.$ === undefined && (window.$ = Zepto)
吐槽一下源碼中不寫分號,總感受怪怪的。
使用自執行匿名函數返回$
傳遞給Zepto
。而後把Zepto
和$
做爲window
的屬性。
這樣對外只有兩個或一個變量可使用,不會污染全局環境,若是命名衝突,只需改源碼中最後兩行便可。
var Zepto = (function() { var $,zepto = {}; $.trim = function(str) { return str == null ? "" : String.prototype.trim.call(str) } $ = function(selector, context){ return zepto.init(selector, context) } $.fn = { addClass: function(name){ // 省略 } // 省略 } zepto.Z.prototype = Z.prototype = $.fn $.zepto = zepto return $ })()
在此能夠看到$.trim與addClass與其餘變量或屬性的關係,去除這兩個屬性後就是第二層的架構,以下。
var $,zepto = {};
初始化了一個$
變量和zepto
對象,注意這裏是小寫
$
函數
這個函數調用zepto.init
方法,返回對象,在以後會講解。
添加$.fn
對象
它擁有Zepto對象上全部可用的方法(官方文檔),這裏可能有誤解,應該是擁有由$()
返回的對象的全部方法,裏面的方法在$("#person").prototype
中看到過
zepto.Z.prototype = Z.prototype = $.fn
Z.prototype = $.fn
若是你仔細觀察開始時的$("#person")
返回的對象其實就是Z,那麼通過$()
返回的對象的原型指向了擁有大量方法的$.fn
對象,因此才能夠在$("#person").prototype
中看到過addClass
方法
而後是zepto.Z.prototype = $.fn
,請參考zepto源碼中關於zepto.Z.prototype = $.fn的問題
$.zepto = zepto
不知道爲何有這一句,彷佛是能夠經過$.zepto訪問內部的方法,例如$.zepto.isZ($("#person"))
。又或許是想將其封裝爲$
的屬性。
return $
能夠清楚的看到內部的結構,$.fn
、$.zepto
、$.trim
都做爲$對象的屬性存在,若是調用$()
函數,返回的Z對象就擁有指定的原型鏈Z.prototype = $.fn
。
那麼問題又來了:zepto.init
方法是作什麼的?執行$()
函數返回的是什麼對象?
zepto.init
仍是先上源碼
$ = function(selector, context){ return zepto.init(selector, context) } zepto.init = function(selector, context) { var dom if (!selector) return zepto.Z()// 若是是$()或$("")則執行 else if (typeof selector == 'string') {// 若是傳入的是字符串 selector = selector.trim()// 去除收尾空白符 if (selector[0] == '<' && fragmentRE.test(selector))// 若是傳入的字符串是以<開頭且符合HTML代碼規則(用了正則表達式),即建立元素 dom = zepto.fragment(selector, RegExp.$1, context), selector = null// 建立一個DOM對象 else if (context !== undefined) return $(context).find(selector)// 這裏實際上是一種討巧的辦法,我相信jQuery中確定不會這麼寫,目的是實如今指定範圍內查找[context]元素 else dom = zepto.qsa(document, selector)// 調用zepto.qsa解析字符串,返回一個DOM數組 } else if (isFunction(selector)) return $(document).ready(selector)// 很簡單,若是是函數,則在文檔就緒後執行 else if (zepto.isZ(selector)) return selector// 若是是一個zepto對象,直接返回 else { if (isArray(selector)) dom = compact(selector)// 若是是數組,調用compact返回一個數組,最後經Z變成類數組對象,我想這裏是把幾個DOM對象做爲數組的參數傳入,返回一個類數組對象 else if (isObject(selector))// 若是是一個對象,將其包含在數組以內,如p = document.getElementById("#p");$(p); dom = [selector], selector = null else if (fragmentRE.test(selector))// 不知道是幹嗎的 dom = zepto.fragment(selector.trim(), RegExp.$1, context), selector = null else if (context !== undefined) return $(context).find(selector) else dom = zepto.qsa(document, selector) } return zepto.Z(dom, selector)// 能夠看這裏,不管以上過程經歷了什麼,都要通過此函數,目的是將數組轉化爲類數組對象。 } zepto.Z = function(dom, selector) { return new Z(dom, selector) } /** * 一個構造函數,將dom對象中的屬性和方法都複製到this下,並添加了兩個屬性,length和selector,這個函數的目的是將DOM對象轉化爲供zepto使用的類數組對象 */ function Z(dom, selector) { var i, len = dom ? dom.length : 0 for (i = 0; i < len; i++) this[i] = dom[i] this.length = len this.selector = selector || '' } zepto.fragment = function(html, name, properties) { // 這裏的代碼就不展開了,其做用是返回一個DOM對象,若$()中傳入第二個參數,則將其屬性添加給建立的DOM對象 return dom } zepto.qsa = function(element, selector){ // 這裏也不展開代碼,又興趣的能夠直接看源碼,很簡單,無非是根據傳入的選擇符分別調用getElementByID、getElementsByTagName、getElementsByClassName、querySelectorAll等方法,返回一個數組,數組的值便是DOM對象,這就是最核心的選擇器,有點坑爹。 }
這一段代碼是zepto的核心代碼,是使用zepto的入口,這裏仍是從文檔入手比較好理解。
Zepto集合是一個相似數組的對象,它具備鏈式方法來操做它指向的DOM節點,除了$(Zepto)對象上的直接方法外(如$.extend),文檔對象中的全部方法都是集合方法。
上一句能夠告訴咱們:Zepto集合是一個相似數組的對象便是以前$("#person")
返回的對象。文檔中全部不帶前綴的方法叫作集合方法。
再來看方法的調用
$(selector, [context]) // 在指定範圍內查找[context]元素,相似$(context).find(selector),例如$(".logo",".header"),只選取.header類中的.logo類 $(<Zepto collection>) // 這裏應該是指傳入zepto對象,如:var a = $("a");$(a); $(<DOM nodes>) // 選取全部頁面中的div元素,如:$('div') $(htmlString) // 建立一個元素,如:$("<p>Hello</p>") $(htmlString, attributes) // 建立帶有屬性的元素,$("<p />", { text:"Hello", id:"greeting", css:{color:'darkblue'} }) Zepto(function($){ ... }) // 當頁面ready的時候,執行回調 // 還有寫文檔中沒有 $()或$("")
至於爲何調用Z()函數返回的對象都以Z爲對象名呢?看這段小代碼就能夠明白
function Z(){ this.name = 666; } z = new Z(); console.log(z);// 返回的對象名爲Z
這個過程比較複雜,建議你親自動手試一試,仍是以$("#person")
爲例,用Chrome在這一行打斷點,而後步進,我看能不能作個flash圖。
若是你仔細觀察,這一層的核心源碼最後大部分都有return返回,最終$()
也會返回對象,整個過程實際上是對向$()
中傳入的參數進行處理運算,最終返回一個zepto本身創造的對象,而後用於後續操做。
zepto的源碼對通常的熟練面向對象的人來講是很是簡單的,對於有面向對象概念沒有寫過的人來講是那種踮起腳尖能獲得的難度。最開始想學習jQuery源碼,但看了一點以爲太複雜,因而投機取巧看zepto,也是徹底理不清頭緒啊,知道上個星期天找到了一種方法,寫一端小代碼,而後在Chrome裏步進調試,看函數之間的依賴關係法,看函數的傳入值,返回值,瞭解這個函數是作什麼用的。最後慢慢的理清頭緒。這篇文章在星期一就開始寫,一直到星期四纔算完成。
$
(或Zepto
)是一個函數對象,但他包含了一些特定的屬性(方法)。能夠直接調用這些屬性(方法),這些屬性(方法)大都與瀏覽器無關。也能夠執行$
函數,執行後返回一個類數組對象,這個對象的原型中包含一些操做DOM的方法,向原型中添加屬性(方法),全部的對象均可以訪問到。執行$
函數是zepto的關鍵代碼,其目的是根據傳入函數的變量值,加工處理成類數組對象並返回,用於後續操做。
同時發表在個人博客:《zepto源碼分析-代碼結構》