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

餘大彬發表於2018-08-08

如何讀原始碼

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(/ jQueryd+="(?: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:寫文章不宜,如果這篇文章對您有幫助的話,希望您多多點選推薦哦!

相關文章