如何讀源碼
jQuery
整體框架甚是復雜,也不易讀懂。但是若想要在前端的路上走得更遠、更好,研究分析前端的框架無疑是進階路上必經之路。但是龐大的源碼往往讓我們不知道從何處開始下手。在很長的時間里我也被這種問題困擾着,自己也慢慢摸索到一個比較不錯的看源碼的“姿勢”。
一定不推薦的就是拿到源碼直接開始啃,首先我們一定要對這個框架的整體的架構有一定的了解,每個模塊之間的聯系是怎樣的;
然后找一找有沒有關於源碼分析的書籍,如果有的話那么恭喜你了,你可以直接跟着書的思路開始看源碼;
如果沒有框架源碼分析相關書籍的話,那么就只能自己啃源碼了,可以從成熟框架的早期源碼開始看起,這樣一開始的代碼量不多,多看幾遍還是可以理解的。
看源碼時不僅要知道其然,還要知道其所以然。即不僅要知道這樣寫,還需要知道為什么這樣寫。這就要求我們不僅要看源碼,而且要敲源碼,換幾種不同的思路來實現源碼實現的功能能讓我們更好的理解作者為什么這樣寫。
------------------------------------------------------------------------------------------分隔線,下面介紹jQuery框架的實現核心思路.
為方便閱讀和理解,其核心代碼只有70幾行。 源碼鏈接請點擊這里,如果對您有用的話,歡迎star。
1、jQuery框架總體架構
(function(){ //替換全局的$,jQuery變量 var _jQuery = window.jQuery, _$ = window.$, //jQuery實現 jQuery = window.jQuery = window.$ = function( selector, context ) { return new jQuery.fn.init( selector, context ); }; //jQuery原型方法 jQuery.fn = jQuery.prototype = { init: function( selector, context ) {}, //一些原型的屬性和方法 }; //原型替換 jQuery.fn.init.prototype = jQuery.fn; //原型擴展 jQuery.extend = jQuery.fn.extend = function() { ... }; jQuery.extend({ // 一堆靜態屬性和方法 }); })();
2、$()實現細節
我們知道使用jQuery的唯一入口就是全局屬性jQuery、$。我們可以先實現一個jQuery類。
var $ = jQuery = function () { }; jQuery.fn = jQuery.prototype = { name : "jQuery", size : function () { return this.length; } }; var my$ = new $(); console.log(my$.name);
其實直接用jQuery生成一個jQuery實例,也可以實現jQuery框架相同的效果。但是jQuery框架並沒有使用new為jQuery類創建一個新實例,而是直接調用jQuery()方法,然后在后面鏈式調用原型鏈上的方法。如下所示:
$().size()
這是怎么實現的呢?也就是說我們需要把jQuery即看作是一個類,同時又是一個普通的函數。而這個函數調用返回jQuery類的實例。
var $ = jQuery = function () { return new jQuery();//返回類的實例 }; jQuery.fn = jQuery.prototype = { name : "xiaoyu", size : function () { return this.length; } }; var my$ = $(); console.log($().name); //Uncaught RangeError: Maximum call stack size exceeded
執行上述代碼,提示內存外溢的錯誤,說明執行$()時出現了循環引用。可見執行$()不能返回jQuery的實例,而應該返回其它類的實例才不會導致棧溢出。實際上jQuery也是這么做的。
那么如何返回一個類的實例呢?
var $ = jQuery = function () { return new jQuery.fn.init();//產生一個init()的實例 }; jQuery.fn = jQuery.prototype = { init: function() { console.log(this); return this; }, name : "xiaoyu", size : function () { return this.length; } }; console.log($().__proto__ === jQuery.fn.init.prototype);//$().__proto__ -> init.prototype
執行上述代碼,執行$()返回了一個實例對象,這已經很接近jQuery框架的。
但是還有一個原型指向問題:在jQuery中,執行$()函數返回的實例對象的__proto__指向的是jQuery()函數的prototype屬性,而我們自己實現的jQuery類執行$()返回的實例對象的__proto__指向的是init()函數的prototype屬性。
所以我們在執行$()函數之前,還需要手動改變init()函數的prototype指向,使其指向jQuery.prototype。
var $ = jQuery = function () { return new jQuery.fn.init();//產生一個init()的實例 }; jQuery.fn = jQuery.prototype = { init: function() { console.log(this); return this; }, name : "xiaoyu", size : function () { return this.length; } }; //在實例化前,將init.prototype覆蓋為jQuery.prototype jQuery.prototype.init.prototype = jQuery.prototype; console.log($().__proto__ === jQuery.prototype);//$().__proto__ -> jQuery.prototype
3、實現一個簡易的DOM選擇器
第二講我們已經完成了jQuery框架的基本的實現:執行$()函數能夠返回一個jQuery對象。
我們說過$()函數包含兩個參數selector和context。其中selector表示選擇器,context表示選擇器的選擇的內容范圍。$()函數執行返回的是一個jQuery對象,是一個類數組對象。本質上是一個對象,雖然擁有數組的length和index,卻沒有數組的其他方法。
在jQuery中,假如我們需要操作一個DOM元素,我們可以這樣選中它。
$('div').html("hello");//選中document下的所有div標簽,並設置所有選中的DOM元素的innerHTML內容
下面我們就實現一個簡易的標簽選擇器的功能。
核心思路是:
- 通過傳入的selector參數,操作原生JS來實現DOM元素的過濾,獲取我們需要的DOM元素集合,並將DOM元素集合作為屬性添加到jQuery對象中,並返回jQuery對象。
- 實現鏈式操作是通過在上一步操作結束時返回jQuery對象。
var $ = jQuery = function (selector,context) { //定義類 return new jQuery.fn.init(selector,context); //返回選擇器的實例 }; jQuery.fn = jQuery.prototype = { //jQuery的原型對象 init: function(selector,context) { //定義選擇器的構造器 selector = selector || document; //默認值為document context = context || document; //默認值為document if (selector.nodeType) { //如果傳入的參數是DOM節點 this[0] = selector; //把參數節點傳遞給實例對象的index this.length = 1; //設置長度為1 this.context = selector; return this; //返回jQuery對象 } if (typeof selector === 'string') {//如果傳進來的是標簽字符串 let ele = document.getElementsByTagName(selector); //獲取指定名稱的元素 for (let i = 0; i < ele.length; i++) { //將獲取到的元素放入實例對象中 this[i] = ele[i]; } this.length = ele.length; return this; } else { this.length = 0; this.context = context; return this; } }, name : "jQuery", size : function () { return this.length; } }; jQuery.prototype.init.prototype = jQuery.prototype;
let div = $('div').size();
如上所述的代碼,$()函數已經基本傳入DOM元素和元素標簽返回一個jQUery對象的功能。
通過上面實現的一個簡易的DOM選擇器,我們知道:jQuery對象是通過jQuery框架包裝DOM對象后產生的一個新的對象。框架為jQuery對象定義了獨立的方法和屬性(定義在jQUery.prototype原型屬性上),因此jQuery對象無法直接調用DOM對象的方法,DOM對象也無法直接調用jQuery對象的方法。
我們也可以很輕易地實現jQuery對象和DOM對象的相互轉換。
- jQuery對象轉換為DOM對象:借助jQuery對象的類數組下標選擇jQuery對象中的某個DOM元素。
- DOM元素轉換為jQuery對象:直接把DOM元素當作參數傳遞給$()函數,$()函數會自動把DOM對象包裝為jQuery對象。
3.1、實現$('div').html("hello")功能
核心思路:在原型上封裝一個html()函數,根據傳遞進來的參數來判斷是獲取第一個DOM元素的innerHTML還是設置每一個DOM元素innerHTML。
var $ = jQuery = function (selector,context) { //定義類 return new jQuery.fn.init(selector,context); //返回選擇器的實例 }; jQuery.fn = jQuery.prototype = { //jQuery的原型對象 init: function(selector,context) { //定義選擇器的構造器
//省略初始化構造器的主體代碼
}, constructor: jQuery, //定義jQuery中的html()方法 html: function(val) { if (val) { for(let i = 0; i < this['length']; i++){ this[i].innerHTML = val; } }else { return this[0].innerHTML; } }, name : "jQuery", size : function () { return this.length; } }; jQuery.prototype.init.prototype = jQuery.prototype; let div = $('div').html('hello');
OK!一個簡易的html()函數的功能已經實現完成了,我們可以看一下jQuery源碼是如何實現的。以便學習別人的編程思想。
html: function( value ) { return value === undefined ? (this[0] ? this[0].innerHTML.replace(/ jQuery\d+="(?:\d+|null)"/g, "") : null) : this.empty().append( value ); },
//源碼使用三目運算符判斷參數是否為空,如果為空,則返回第一個元素的innerHTML;若不為空,則先清空匹配元素中的內容,並使用append插入值。
4、功能擴展函數extend
根據一般的習慣,如果要為jQuery或者jQuery.prototype添加函數或方法,可以直接通過"."語法實現,或者在jQuery.prototype對象上添加一個屬性即可。
但是分析jQuery源碼可以知道jQuery是通過extend()函數來實現擴展功能的,即插件功能。
這樣做有什么好處呢?
extend能夠方便用戶快速的擴展jQuery框架的功能,但不會破壞jQuery框架的原型結構從而避免后期人工手動添加工具函數或方法時破壞jQuery結構的單純性。
同時也方便管理。如果不需要某個插件時簡單的刪除掉即可,而不需要在jQuery框架源代碼中去刪除。
我們自己也可以實現一個簡單的函數擴展功能,只需把指定對象的方法復制給jQuery對象或者jQuery.prototype對象。
//接受一個對象作為參數(實現批量的擴展)
jQuery.extend = jQuery.prototype.extend = function (obj) { for (let key in obj) { if (obj.hasOwnProperty(key)) { this[key] = obj[key]; } } return this; }
5、命名空間問題
但還需要考慮的一個問題就是命名空間的問題:當一個頁面中存在多個框架或者眾多代碼時,我們是很難確保代碼不發生沖突的。
所以難免會出現命名沖突或代碼覆蓋的現象。我們必須把jQuery代碼封裝在一個孤立的環境中,避免其他代碼的干擾。
我們可以通過匿名函數執行,形成閉包,將代碼封裝在一個封閉的環境中,只通過唯一的入口window.jQuery訪問。
(function(){ var jQuery = window.jQuery = window.$ = function( selector, context ) { // The jQuery object is actually just the init constructor 'enhanced' return new jQuery.fn.init( selector, context ); }; })(window);
5.1、命名沖突
同時,為了防止同其他框架協作時發生$簡寫的沖突,我們可以封裝一個noConflictl()方法解決$簡寫沖突。
思路分析:在匿名執行jQuery框架的最前面,先用_$,_jQuery兩個變量存儲外部的$,jQuery的值。執行noConflict()函數時再恢復外部變量$,jQuery的值。
(function(){ var window = this, _jQuery = window.jQuery,//存儲外部jQuery變量 _$ = window.$,//存儲外部$變量 jQuery = window.jQuery = window.$ = function( selector, context ) { return new jQuery.fn.init( selector, context ); }; jQuery.noConflict = function( deep ) { window.$ = _$;//將外部變量又重新賦值給$ if ( deep ) window.jQuery = _jQuery;//將外部變量又重新賦值給jQuery return jQuery; }, })();
至此,我們已經模擬實現了一個簡單的jQuery框架。以后就可以根據x項目需要不斷的擴展jQUery的方法即可。
PS:寫文章不宜,如果這篇文章對您有幫助的話,希望您多多點擊推薦哦!