天天看點

手寫JQuery 的架構的實作

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向下相容
    • div.animate({left: 200}, 1000)

      在2.2.4版本之後編譯不會報錯,但是沒有動畫效果(即為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);
    }

}