從零實現一個簡易的jQuery框架之二—核心思路詳解

如何讀源碼

jQuery總體框架甚是複雜,也不易讀懂。可是若想要在前端的路上走得更遠、更好,研究分析前端的框架無疑是進階路上必經之路。可是龐大的源碼每每讓咱們不知道從何處開始下手。在很長的時間裏我也被這種問題困擾着,本身也慢慢摸索到一個比較不錯的看源碼的「姿式」。html

必定不推薦的就是拿到源碼直接開始啃,首先咱們必定要對這個框架的總體的架構有必定的瞭解,每一個模塊之間的聯繫是怎樣的;前端

而後找一找有沒有關於源碼分析的書籍,若是有的話那麼恭喜你了,你能夠直接跟着書的思路開始看源碼;node

若是沒有框架源碼分析相關書籍的話,那麼就只能本身啃源碼了,能夠從成熟框架的早期源碼開始看起,這樣一開始的代碼量很少,多看幾遍仍是能夠理解的。git

看源碼時不只要知道其然,還要知道其因此然。即不只要知道這樣寫,還須要知道爲何這樣寫。這就要求咱們不只要看源碼,並且要敲源碼,換幾種不一樣的思路來實現源碼實現的功能能讓咱們更好的理解做者爲何這樣寫。github

------------------------------------------------------------------------------------------分隔線,下面介紹jQuery框架的實現核心思路.編程

爲方便閱讀和理解,其核心代碼只有70幾行。 源碼連接請點擊這裏,若是對您有用的話,歡迎star。數組

一、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({ // 一堆靜態屬性和方法
 }); })(); 

二、$()實現細節

咱們知道使用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類的實例。app

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

三、實現一個簡易的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.一、實現$('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插入值。

四、功能擴展函數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; }

五、命名空間問題

但還須要考慮的一個問題就是命名空間的問題:當一個頁面中存在多個框架或者衆多代碼時,咱們是很難確保代碼不發生衝突的。

因此不免會出現命名衝突或代碼覆蓋的現象。咱們必須把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.一、命名衝突

同時,爲了防止同其餘框架協做時發生$簡寫的衝突,咱們能夠封裝一個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:寫文章不宜,若是這篇文章對您有幫助的話,但願您多多點擊推薦哦!

相關文章
相關標籤/搜索