手寫JQuery的框架的簡易實現(一步一步教你庫開發的架構設計)

樑音發表於2018-06-06

JQuery的好處

  1. 快速上手(學習成本低)
  2. 開發效率高(選擇器、批量操作 DOM、鏈型操作……)
  3. 一系列的封裝(動畫、ajax)
  4. 瀏覽器相容(1.x版本 相容IE6、7、8)
  • JQuery 1.11.3.js(1.x經典版本)
    • 效能不好(原始碼檔案略大)
  • JQuery 2.2.4(2.x經典版本)
    • 效能略好(原始碼檔案略小)
    • 不是 HTML5 的實現
  • 2.x版本對於1.x版本來說,API向下相容
    • 2.x版本比1.x版本效能好
    • IE8及以下版本不支援HTML5,所以2.x版本放棄相容

實現自己的jQuery

第一步:分析

<span>this is span</span>
<div class="my-div">this is first div</div>
<div class="my-div">this is second div</div>
複製程式碼
// 第一個問題:想想這句話實際上是執行了什麼樣的操作?
$(".my-div")
// 解決了第一個問題後,想想如何進行批量操作?
$(".my-div").css("color","red")
$(".my-div").html("--------------")
複製程式碼
// 分析
// 通過原生方法只能執行下面操作
        //通過ID找到唯一元素
document.getElementById()
        //通過標籤名找到所有標籤名對應的元素
        .getElementsByTagName()
        //通過name屬性找到所有對應的元素
        .getElementsByName()
        //是通過selector選擇器找到所有符合選擇器的元素(方便、快捷、HTML5支援)
        .querySelectorAll()
複製程式碼
// mineJQuery
var $ = function(selector){
    // $選擇符已經定義完畢
    return document.querySelectorAll(selector)
    // return 了一個陣列[div.my-div, div.my-div]
    // 陣列物件沒有方法,那.css該如何進行批量操作?
    
    // 除非進行一個 for 迴圈,接著上面的問題。
    for (var i = 0; i < $(".my-div").length; i++) {
        $(".my-div")[i].style.color = "red";
    }
    // 如果這樣操作則使用 jQuery 一點意義都沒有 
}
複製程式碼

第二步:構造器函式和普通函式及原型(for迴圈太多,降低效率)

    // 構造器函式是用來通過new關鍵字建立一個例項(一般構造器函式都是大寫開頭)
    // 先在池中開闢區間存放變數Proxy
    // 再在堆中開闢區間存放function
    var Proxy = function(selector){
        // this指的是什麼呢?誰呼叫這個函式,指的就是誰!
        // 找到的所有的目標dom元素陣列儲存在this.doms中
        this.doms = document.querySelectorAll(selector)
    };
    // 函式原型
    Proxy.prototype = {
        css: function(style, value){
            for(var i = 0;i < this.doms.length;i++){
                this.doms[i].style[style] = value;
            }
            // 那麼鏈式操作該怎麼進行呢?答案很簡單。
            
            // 返回當前例項,目的是讓接下來又可以訪問Proxy.prototype的方法(即可以進行原型鏈操作a.b.c.d)
            return this;
        },
        html:function(html){
            for(var i = 0;i < this.doms.length;i++){
                this.doms[i].innerHTML = html;
            }
            return this;
        }
    };
    // 生命週期很長
    // new一個關鍵字相當於在堆中開闢一個新的空間,都繼承了母體(Proxy)的特徵,特徵全部集中在prototype的原型裡
    var p = new Proxy();
    
    var $ = function(selector){
        return new Proxy(selector);
    }
    
    // 基本功能已經實現了,很奈斯!
    // 那麼問題又來了,每個原型裡面的方法都要進行 for 迴圈會顯得很繁瑣,該如何進行優化呢?
複製程式碼
    //PS.----------------------------------------
    // 普通函式
    // 直接在堆中開闢區間執行function(包括了從池中取常量等一系列操作,和閉包相似)
    // 其中各項操作完成後生命週期就結束(記憶體就被回收)
    var func = function(){
        var a;
        var b = 10;
        a = 20;
    }
    // 呼叫函式
    func();
複製程式碼
棧(最小) 堆(最大) 池(擺放常量的地方)(中間)
Proxy function(構造器函式) 0-9、a-z、A-Z、各種符號
X(無) function(普通函式)

第三步:改進

  • 程式設計最基本思維:程式碼複用
     Proxy.prototype = {
        // 好處:如果需要修改迴圈結構,只需要修改each,大大增強程式碼維護性
        each: function(callback){
            for(var i = 0;i < this.doms.length;i++){
                // call呼叫:還是呼叫Proxy函式
                // i => index, this.doms[i] => object
                callback.call(this, i, this.doms[i]);
            }
        },
        css: function(style, value){
            this.each(function(index, object){
                 object.style[style] = value;
                })
            // 返回當前例項,目的是讓接下來又可以訪問Proxy.prototype的方法(即可以進行原型鏈操作a.b.c.d)
            return this;
        },
        html:function(html){
             this.each(function(index, object){
                 object.innerHTML = html;
                })
            return this;
        }
    };
    
    // 問題:如果程式設計師在自己的js檔案中寫,var Proxy = function(){}
    // 則會與我們寫的jQuery方法名衝突
    
    // 原因:是我們寫的jQuery裡面Proxy是一個全域性變數
    // 那該如何進行改進呢?
複製程式碼

第四步:繼續改進

    // 建立閉包
var $ = jQuery = (function(){
     var Proxy = function(selector){
        this.doms = document.querySelectorAll(selector)
     };
      Proxy.prototype = {
        // 好處:如果需要修改迴圈結構,只需要修改each,大大增強程式碼維護性
        each: function(callback){
            for(var i = 0;i < this.doms.length;i++){
            // call呼叫:還是呼叫Proxy函式
                callback.call(this, i, this.doms[i]);
            }
        },
        css: function(style, value){
            this.each(function(index, object){
                 object.style[style] = value;
                })
            // 返回當前例項,mu'di'shi目的是讓接下來又可以訪問Proxy.prototype的方法(即可以進行原型鏈操作a.b.c.d)
            return this;
        },
        html:function(html){
             this.each(function(index, object){
                 object.innerHTML = html;
                })
            return this;
        }
    };
    // return 出去這個函式
    return function(selector){
        return new Proxy(selector);
    }
})();
// 是不是看起來有模有樣很完美了?
// 還是會出現問題:console.log($(..)) ==> Proxy {doms: NodeList[..]}是一個物件,並不是一個陣列物件,和原生 jQuery 不一樣。
// 那有什麼影響呢?
// $(..).length 是屬於陣列的方法,而我們模仿的 jQuery 是個物件,並沒有這個屬性。
// 那該如何改進呢?
複製程式碼

第五步:繼續改進

// 有人會說我直接在 Proxy 方法里加一個 length 就可以了
    var Proxy = function(selector){
        this.doms = document.querySelectorAll(selector);
        this.length = this.doms.length;
    };
// 這樣做確實能達到效果,但是有個問題,大家先想想有什麼問題。


// 問題就是,jQuery 返回的是個陣列物件,但是陣列物件不可能只有一個 length 屬性,還有很多其他功能,難道我們要把所有陣列的功能都加進來嗎,明顯是不現實的。
// 所以程式碼需要重新重構一下
複製程式碼
var $ = JQuery = (function(){
    // 建立一個陣列
    function markArray(selector) {
        var arr = document.querySelectorAll(selector);
        return arr;
        // 此時返回的確實是一個陣列物件,但是感覺一夜回到瞭解放前,對比看下和真正的 jQuery 的陣列物件有什麼差別?
    }
    return function(selector) {
        return markArray(selector);
    }
})()

複製程式碼

對嘛!缺什麼補什麼嘛!

var $ = JQuery = (function(){
    // 建立一個陣列
    function markArray(selector) {
        var arr = document.querySelectorAll(selector);
        // console.log(arr.constructor) 看看是個啥
        
        
        // 對 native code 那麼 native code 是什麼呢?
        // 接地氣點講就是系統自帶的本地實現,還說得直接一點就是呼叫系統本地的 C 原始碼庫,類似 java 裡面 JNI 呼叫 系統的 C 語言
        
        // 繼續往下講,這個時候我們就能拿到 NodeList ,相當於構造器類
        // 也就是說我們寫上 NodeList.prototype.html = function(html){alert(html)}
        // 也就是在 arr 的原型上繫結了這個方法,這個時候就能生效了!
        // 所以,我們要學會在原生的程式碼裡擴充套件功能
        return arr;
    }
    NodeList.prototype.each = function(callback) {
        // 這個時候可以直接用 this.length,想想為什麼?
        for (var i = 0; i < this.length; i++) {
            callback.call(this, i ,this[i])
        }
    }
    NodeList.prototype.html = function(html) {
            this.each(function(index, object) {
                object.innerHTML = html;
            })
    }
    return function(selector) {
        return markArray(selector);
    }
})()

// 改造大功告成,完美執行,可能有人有疑惑了,為啥不用簡單方法寫呢,例子如下
複製程式碼
// 用眼看不如實際操作一下,你會發現控制檯報錯了!
var $ = JQuery = (function(){
    // 建立一個陣列
    function markArray(selector) {
        var arr = document.querySelectorAll(selector);
        return arr;
    }
    NodeList.prototype = {
        each: function(callback) { 
            for (var i = 0; i < this.length; i++) {
                callback.call(this, i ,this[i])
            }
        },
        html: function(callback) {
            this.each(function(index, object) {
                object.innerHTML = html;
            })
        }
    }
    return function(selector) {
        return markArray(selector);
    }
})()

// 大家注意一下,本身這個思路是沒問題的,問題還是出現在 NodeList 是一個 native code ,native code 的原型是不允許被變更的,而上面的操作直接強制改變了 NodeList 的原型,所以只能增加。
// 那麼有什麼辦法呢?
複製程式碼

注意了,下面的寫法可能會改變你寫程式碼的思維

// 這個架構和之前的架構有本質上的區別
var $ = JQuery = (function(){
    function markArray(selector) {
        var arr = document.querySelectorAll(selector);
        return arr;
    }
    // 第一步:定義一個區域性變數
    var $ = function (selector) {
        return markArray(selector)
    }
    // 自己實現繼承
    $.extend = function(target) {
        // 如何進行擴充套件呢?
        // 我們需要用到 for 迴圈
        // 為什麼要從1開始?
        // 因為 在函式內部有一個 arguments 物件, arguments 這個引數代表的是,呼叫這個函式所有的引數,即 NodeList 和擴充套件內容,我們只需要遍歷擴充套件內容
        for (var i = 1; i < arguments.length; i++) {
            // 再次迴圈遍歷出擴充套件的內容
            for(var prop in arguments[i]) {
                // 一個個加到這個裡面來
                target[prop] = arguments[i][prop]
            }
        }
        // ruturn 給了$.fn
        return target;
    }
    // 第二步:基於 NodeList 進行擴充套件,將後面所有的物件全部擴充套件到這個裡面去,這個思路要好好捋一捋
    // 直接說了,這個程式碼的意思就是,我呼叫了一個繼承的方法,這個繼承的方法在上面自己寫,用來傳入一些引數,進行擴充套件 
    // 相當於(target,exd1,exd2,exd3)將 exd1,exd2,exd3的方法擴充套件給 target
    // 當 target return 出去的時候,這個時候的 target 就是一個超級 NodeList
    // $.fn 的用途以後再說
    $.fn = $.extend(NodeList.prototype, {
        each: function(callback) { 
            for (var i = 0; i < this.length; i++) {
                callback.call(this, i ,this[i])
            }
            // 要想能進行鏈式操作,所有方法都需要 return this,但其實只需要在 each 方法裡 return 就可以了,其他方法只需要將自己的呼叫 return 出來
            return this;
        },
        html: function(callback) {
            return this.each(function(index, object) {
                object.innerHTML = html;
            })
        }
    })
    return $;
})()
複製程式碼
// jQuery 裡有一個外掛機制,就是利用$.fn 可以擴充套件 jQuery 的方法,做到自己定製 jQuery
// 例如 jQuery 自帶的和我實現的都觸發了這個方法:
$.fn.altr = function(){
    alert(1);
}
$(".my-div").altr();
// 有些人會奇怪了,為什麼我沒有寫這個方法,但是還是能正確觸發了這個方法呢?
// 原因在於 $.fn 指向了 NodeList.prototype這個物件,可以在這個之上繼續擴充套件。
// 不信的人可以去試試 console.log($.fn === NodeList.prototype)
// 類似$(function(){})這種的,都是在此型別上繼續擴充,進行判斷
var $ = function (selector) {
    if (typeof selector === "function") {
        window.onload = selector
    } else {
        return markArray(selector);
    }

}
複製程式碼

相關文章