jquery是一個強大的js類庫,提供了不少便利的操做方法併兼容不一樣的瀏覽器,一旦使用便欲罷不能,根本停不下來,今天咱們就來解讀一下這個神祕的jquery源代碼。html
前幾天思考再三,本身嘗試着封裝了一下jquery的$選擇器,然而並不完善,我只對id,class,和標籤選擇器進行了封裝,發現其實若是實現淺層的封裝那麼咱們很容易就可以實現,可是一旦咱們嘗試着選擇器的層次嵌套就會出來不少大大小小的坑!node
下面咱們先來看一下我我的封裝的jquery的選擇器部分。jquery
window.$ = function (selector) { if (typeof selector === 'object') { console.log(selector,123) } else if (typeof selector === 'string') { var arr = selector.split(' ') for(var i=0;i<arr.length;i++){ oArr = [] if(arr[i].charAt(0)=='#'){ var a = arr[i].substr(1) // oEle = oEle.getElementById(a) if(!oEle.length){ oEle = oArr= document.getElementById(a) }else{ for(var j=0;j<oEle.length;j++){ // console.log(oEle[j]) for(var s = 0;s<oEle[j].childNodes.length;s++){ // console.log(oEle[j].childNodes[s]) if(oEle[j].childNodes[s].nodeType==1){ // console.log(oEle[j].childNodes[s].nodeType) if(oEle[j].childNodes[s].getAttribute('id')==a){ oArr[j] = document.getElementById(a) // console.log('yes',s) } // console.log(oEle[j].childNodes[s].getAttribute('id')) } } } oEle = oArr } }else if(arr[i].charAt(0)=='.'){ // console.log(oEle.length) // console.log(oEle) var a = arr[i].substr(1) if(!oEle.length){ oEle = oArr= oEle.getElementsByClassName(a) // console.log(oEle,'class') }else{ for(var j=0;j<oEle.length;j++){ // console.log(oEle) if(oEle[j].getElementsByClassName(a).length){ // console.log(oEle) // console.log(1,oEle.length) oArr[j] = oEle[j].getElementsByClassName(a) } // console.log(oEle[j].getElementsByClassName(a)) } oEle = oArr } // console.log(oEle) }else{ // console.log(oEle) // console.log(arr[i]) if(!oEle.length){ oEle = oArr = oEle.getElementsByTagName(arr[i]) // console.log(oEle,'tag') }else{ for(var j=0;j<oEle.length;j++){ // console.log(oEle[j].getElementsByTagName(arr[i]),oEle.length) if(oEle[j].getElementsByTagName(arr[i]).length){ oArr[j] = oEle[j].getElementsByTagName(arr[i]) // console.log(oEle[j].getElementsByTagName(arr[i])) // console.log(oEle[j].getElementsByTagName(arr[i]),j) } } oEle = oArr } // var oEle = oEle.getElementsByTagName(arr[i]) // console.log(oEle) } } // console.log(oEle) } // console.log(oEle,12) console.log(oArr) // console.log(oEle.length) // console.log(document.getElementById('c')) // console.log(oEle.getElementById('c')) } window.$('.content #c')
jquery的原理大概就是咱們把一個$暴漏給window,而後咱們在$下面能夠寫不少方法,而後咱們在$以後返回了一個jquery的dom對象在調用本身封裝的jquery庫中的方法來實現各類瀏覽器的兼容。數組
下面咱們來講一下我封裝這個類庫的時候碰到的一些坑吧:promise
首先咱們封裝類庫無非用幾種js原生的選擇器,好比document.getElementById或者document.getElementsByTagName。瀏覽器
這兩個是最經常使用的方法可是這個也是問題的所在,打個比方,咱們id選擇器選到的是一個dom對象,可是咱們getElementsByTagName取到的倒是一個數組。dom
這個問題怎麼解決,由於咱們選擇器支持選擇多個選擇器,好比$('#div div'),咱們選擇id=div的dom元素以後咱們在選擇dom下面的全部的div,才能實現dom節點循環嵌套,可是換一個角度$('div #div'),咱們選擇div標籤下面的id=div的元素,那麼咱們如何進行數組和dom元素的轉化?函數
個人決解的方法是本身定義了一個數組,而後每次dom元素取出來都放在數組裏面,數組的元素也放在這個數組裏面從而實現真正的循環遍歷。fetch
下面咱們來觀看一個jquery是怎麼實現$選擇器的優化
( function( global, factory ) { "use strict"; if ( typeof module === "object" && typeof module.exports === "object" ) { // For CommonJS and CommonJS-like environments where a proper `window` // is present, execute the factory and get jQuery. // For environments that do not have a `window` with a `document` // (such as Node.js), expose a factory as module.exports. // This accentuates the need for the creation of a real `window`. // e.g. var jQuery = require("jquery")(window); // See ticket #14549 for more info. 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 ); } // Pass this if window is not defined yet } )( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {
if ( !noGlobal ) {
window.jQuery = window.$ = jQuery;
}
} )
咱們能夠看到,jquery用了一個函數當即調用,而後把window傳入函數中,這樣作的意義是防治咱們的類庫污染全局變量,可能有的同窗感受這個不是很重要是由於咱們寫的代碼變量比較少,還不設計代碼優化,若是相似jquery的類庫所有寫進window裏面,那你的代碼也就不用運行了!
以後jquery進行了一些是否window異常,是否有commonJS標準等邏輯判斷來加強類庫的健壯性,這裏咱們就不作過多的解釋了。
以後咱們能夠在源代碼中看到這樣一段代碼
jQuery = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' // Need init if jQuery is called (just allow error to be thrown if not included) return new jQuery.fn.init( selector, context ); },
這裏的jquery其實就是$,jquery類庫最後運行了這樣一句話
window.jQuery = window.$ = jQuery;
因此咱們的jquery其實就是$ ,咱們在$方法傳入兩個參數,第一個參數爲你輸入的選擇器,第二個參數爲上下文,以後咱們return了一個新的對象jquery中的fn,這樣的實現是爲了獲得咱們傳入的那個dom節點,由於咱們全部的方法都寫在了window中的$裏面,咱們經過return的形式來獲取dom以後在調用自身的方法,這個形式相似fetch中的promise對象同樣,以後咱們能夠.then執行同樣,二者都有着殊途同歸之妙。
好了咱們接着往下看:
init = jQuery.fn.init = function( selector, context, root ) { var match, elem; // HANDLE: $(""), $(null), $(undefined), $(false) if ( !selector ) { return this; } // Method init() accepts an alternate rootjQuery // so migrate can support jQuery.sub (gh-2101) root = root || rootjQuery; // Handle HTML strings if ( typeof selector === "string" ) { if ( selector[ 0 ] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { // Assume that strings that start and end with <> are HTML and skip the regex check match = [ null, selector, null ]; } else { match = rquickExpr.exec( selector ); } // Match html or make sure no context is specified for #id if ( match && ( match[ 1 ] || !context ) ) { // HANDLE: $(html) -> $(array) if ( match[ 1 ] ) { context = context instanceof jQuery ? context[ 0 ] : context; // Option to run scripts is true for back-compat // Intentionally let the error be thrown if parseHTML is not present jQuery.merge( this, jQuery.parseHTML( match[ 1 ], context && context.nodeType ? context.ownerDocument || context : document, true ) ); // HANDLE: $(html, props) if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { for ( match in context ) { // Properties of context are called as methods if possible if ( jQuery.isFunction( this[ match ] ) ) { this[ match ]( context[ match ] ); // ...and otherwise set as attributes } else { this.attr( match, context[ match ] ); } } } return this; // HANDLE: $(#id) } else { elem = document.getElementById( match[ 2 ] ); if ( elem ) { // Inject the element directly into the jQuery object this[ 0 ] = elem; this.length = 1; } return this; } // HANDLE: $(expr, $(...)) } else if ( !context || context.jquery ) { return ( context || root ).find( selector ); // HANDLE: $(expr, context) // (which is just equivalent to: $(context).find(expr) } else { return this.constructor( context ).find( selector ); } // HANDLE: $(DOMElement) } else if ( selector.nodeType ) { this[ 0 ] = selector; this.length = 1; return this; // HANDLE: $(function) // Shortcut for document ready } else if ( jQuery.isFunction( selector ) ) { return root.ready !== undefined ? root.ready( selector ) : // Execute immediately if ready is not present selector( jQuery ); } return jQuery.makeArray( selector, this ); };
這裏面咱們傳入了選擇器,咱們先判斷了是否是存在這個selecter,若是不存在的話咱們就return this;
以後咱們判斷傳入的這個字符串的狀況的和不是字符串的邏輯,若是傳入的不是字符串,那咱們就看這個元素的nodeType值是否是不爲0,若是不爲0就說明元素存在,多是text,多是dom
對象等等,以後咱們就作一些邏輯return this,若是selecter爲方法的話咱們就判斷跟節點的ready是否是undefined,若是不是的話咱們執行root.ready(),不然就執行selecter方法。
注:這裏的全部this都是jquery的對象,咱們是判斷了對象是否是包的數組,若是是咱們作這樣的操做:
this[ 0 ] = selector; this.length = 1; return this;
咱們取出第一項,以後把它變成jquery對象咱們就能夠繼續調用jquery的方法了,否則咱們獲得了一個數組咱們是不能繼續調用方法的,這裏將會報錯!
以後若是咱們傳入的selecter爲字符串,那麼咱們就進行邏輯判斷
if ( selector[ 0 ] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { // Assume that strings that start and end with <> are HTML and skip the regex check match = [ null, selector, null ]; } else { match = rquickExpr.exec( selector ); }
這裏面是判斷選擇器是不是一個標籤,若是不是標籤的話咱們就執行
match = rquickExpr.exec( selector );
rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,
咱們匹配出的match若是是標籤在前id在後咱們的match不爲空,這樣咱們就繼續運行下面的代碼,這裏也是jquery健壯性的體現!
if ( match && ( match[ 1 ] || !context ) )
以後若是成功說明上面代碼都成立,我門就繼續往下執行
if ( match[ 1 ] ) { context = context instanceof jQuery ? context[ 0 ] : context; // Option to run scripts is true for back-compat // Intentionally let the error be thrown if parseHTML is not present jQuery.merge( this, jQuery.parseHTML( match[ 1 ], context && context.nodeType ? context.ownerDocument || context : document, true ) ); // HANDLE: $(html, props) if ( rsingleTag.test( match[ 1 ] ) && jQuery.isPlainObject( context ) ) { for ( match in context ) { // Properties of context are called as methods if possible if ( jQuery.isFunction( this[ match ] ) ) { this[ match ]( context[ match ] ); // ...and otherwise set as attributes } else { this.attr( match, context[ match ] ); } } } return this; // HANDLE: $(#id) }
以後咱們作了去標籤以後id的邏輯,這裏面的merge是合併的方法,parseHTML是判斷是否有上下文的邏輯(這個上下文基本上我用jquery歷來沒用過- -)。以後咱們同樣都把這個jquery對象return。
基本上的邏輯就是這樣,咱們定義jquery的時候聲明瞭許多變量許多方法,而後咱們吧一個$暴漏在外部,咱們的$內部new了一個jquery對象,而後調用的方法return this來實現jquery的鏈式操做,以後咱們對傳入進來的選擇器作了各類邏輯的判斷增長了健壯性。
不知道你們有沒有注意到,咱們的jquery對象想要變成dom對象咱們須要get(0) 或者[0]。其實咱們的jquery對象就是一個包着dom對象的一個數組對象,因此咱們才能這樣子來取出來咱們我原生dom對象!其實jquery實現的思想很簡單,複雜的是咱們對各個瀏覽器兼容作的邏輯,咱們對各個函數傳入值狀況做了邏輯,其實想實現jquery很簡單,可是想實現真正的健壯性很好的jquery很是費勁了,這些都須要咱們去經驗積累罷了!