淺析jQuery原理並仿寫封裝一個自己的庫

聖槍遊俠發表於2019-04-02

【前言】最近專案忙的腳不沾地,剛剛結束,準備整理一下以前寫的一些學習筆記和技術文章。本文原是很久之前看jq原始碼時寫的片段,隔了很久再看都忘得差不多了。簡單整理出來,做個記錄。

                淺析jQuery原理並仿寫封裝一個自己的庫

為一名前端工程師,jQuery是我們熟的不能再熟的工具之一,其強大的功能和近乎完美的相容封裝使其成為前端領域必備的技能。用了很長時間的jq,卻一直沒有去探索學習過jQuery原始碼,似乎不算是一名合格的前端er。最近趁著空閒研究了一下,jQuery原始碼可謂是深邃如海,奧妙無窮,平時工作中偶爾需要封裝工具,也不必像它這樣面面俱到,但是簡單學習其封裝思路還是很有意義的。後面還要抽時間仔細學習每一塊的原始碼。這裡簡單說一說我對jq框架封裝的理解,並仿照著封裝css的兩個方法。

首先,jQuery的本質是一個封裝了眾多方法的庫

這個庫的框架是一個沙箱

其最外層的框架或者說起骨架如下:

(function(window, undefined) {
    var jQuery = function () {}
    window.jQuery = window.$ = jQuery;//暴露全域性
 })(window)複製程式碼

可以很明顯的看出其框架結構,在一個沙箱中,所有的函式、方法都放在沙箱內部,頂一個一個名為jQuery的函式,這是核心函式,所有的成員都圍繞它運轉。

為什麼要傳入windowundefined,原因很簡單,傳入window是為了精簡程式碼,減少變數檢索時間,window作為形參直接在函式作用域內被檢索到,無需每次再向上查詢全域性,極大地節省了效能提高了效率。

至於undefined,這玩意是為了處理IE8以下的一個小問題,在這些老式瀏覽器中,undefined是可以作為變數名並被重新賦值的,但是新式瀏覽器已經不支援這種做法。這裡傳入undefined就是為了防止undefined被重新賦值。

此時,我們可以在外部直接new一個jQuery的例項物件:

console.log(new jQuery())//空物件,只有一個__proto__屬性複製程式碼

但是顯然,這個物件和我們實際使用的jq物件相去甚遠,甚至看不到相似之處,彆著急,一步步來。

我們的目標是實現類似jq的方式,在外部可拿到jq物件如$('div'),這個jq物件簡化一下,大致是如下結構:

['div','div','div',length:3]複製程式碼

這就意味著,我們必須傳入一個元素選擇器,在建構函式內接收,通過一些方法,返回出來這麼一個物件。

因此,我們先做一個並不嚴謹的假設,假設jQuery是一個建構函式,通過它來建立物件。

在沙箱內部的jQuery‘建構函式’內,我們傳入一個選擇器,通過建構函式new出物件來:

var jQuery = function (selector) {
    var ele = document.querySelectorAll(selector);
    Array.prototype.push.apply(this, ele);
}複製程式碼

docuemnt.querySelectorAll獲取的是一個‘偽陣列’或者說集合,得到的dom物件都存放在ele上面,但是我們需要在jq物件上拿到這些物件,所以我們必須手動的將這些物件新增到例項上。借用陣列的push方法,不僅能夠方便快捷的達到目的,而且陣列的length屬性可以自動更新。這是一個小技巧。

這裡的this指向,毫無疑問就是jQuery的例項物件,因此,我們只需要在jQuery的原型上新增方法,就可以使外部的例項物件訪問到這些方法,這已經非常接近jq的思路了。

在此基礎上,我們簡單給原型上新增幾個方法:

jQuery.prototype.css=function(){
    console.log('hello css')
},
jQuery.prototype.html=function(){ console.log('hello html')
},
...複製程式碼

通過給原型新增方法,jQuery例項物件可以直接訪問使用這些方法,但是這麼做似乎太麻煩了些,如果有幾十上百個方法,每次這麼新增,程式碼太過冗餘。

因此,使用原型替換的思想,改變原型指向到某個物件上,給這個物件新增方法。可以節省很多程式碼。

jQuery.fn = jQuery.prototype = {
   constructor: jQuery,  // 手動新增了丟失的constructor屬性 
   css: function() {
       console.log("css is ok again");
   }, 
   html: function() {
       console.log("html is ok again");
   } 
}複製程式碼

其中,為了書寫方便,我們將jQuery的原型jQuery.prototype賦值給jQuery‘建構函式’的一個屬性 jQuery.fn ,後者可以完全代替前者。

至此,我們在外面new 一個jQuery物件就可以拿到一個接近原版的jq物件了,也可以訪問原型鏈上的方法和屬性。但是似乎還有哪裡不對,new物件這個操作似乎應該在內部完成?沒錯,我們繼續完善它。

在進一步完善我們的小jQuery之前,我們要打個岔,回憶一下工廠函式是怎麼回事。

關於工廠函式:

 作用:建立例項物件,然後把例項物件給返回出去。

function Person(name, age){
     this.name = name;
     this.age = age;
}
//上面是建構函式,我們建立物件的做法:
var xm = new Person("xm", 20)
console.log(xm);//建立了xm物件

var xh = new Person("xh", 21);
console.log(xh);//建立了xh物件複製程式碼

將這個過程封裝一下:

 function $(name, age){//$就是工廠函式
    return new Person(name, age);
 }// 省去外部的new操作,還能得到例項物件

var xm = $("xm", 20);
console.log(xm);

var xh = $("xh", 22);
console.log(xh);//得到兩個例項物件複製程式碼

可見,封裝好的工廠函式可以通過直接呼叫,批量建立物件出來。我們回到jQuery的話題來

按照這個思路,我們將jQuery函式也封裝一下,使之變成工廠函式:

var jQuery = function (selector) {
    return new jQuery(selector);
}複製程式碼

完成了嗎?似乎完成了?但是又好像有哪裡不對,怎兒看著這麼眼熟,這不是隔壁的遞迴函式嗎,自己調自己,把自己玩死了。so。。?難道jQuery不能當做工廠函式嗎?那麼問題來了,他不做誰能做呢?或者,他不是建構函式?聽著有點亂,但還真被我們蒙對了!

實際上,在jQuery中,真正的建構函式,並不是jQuery函式!我們先前的假設要改一改了。

真正的建構函式,另有其人,不兜圈子了,直接上結論:jQuery函式的真正作用是“工廠函式”,正牌兒建構函式是jQuery.fn.init

這個jQuery.fn.init是什麼鬼?怎麼就把正主jQuery趕下位上臺了呢?

直接上結論:這個jQuery.fn.init其實是jQuery函式的原型上的一個方法,它是真正的建構函式,通過它建立物件。

那麼我們前面的程式碼要改改了。該挪窩的挪窩,該上位的上位:

var jQuery = function (selector) {
    return new jQuery.fn.init(selector);
}複製程式碼

init函式去它該去的地方:

jQuery.fn = jQuery.prototype = {
    constructor: jQuery, // 手動新增了constructor屬性
    init: function(selector) {
    var ele = document.querySelectorAll(selector); // this??? ==> init的例項物件 
    Array.prototype.push.apply(this, ele);
    } 
}複製程式碼

經過這麼一改,this的指向發生了變化,原本存放在jQuery原型上的dom物件們,現在變成了init的兒子,this指向了init。但是我們的方法都是存放在jQuery原型上的,難道還要手動搬回來?算了算了,太麻煩,還好有原型鏈這個好東西。手繪了一張草圖,將就看一下:

淺析jQuery原理並仿寫封裝一個自己的庫

init的例項物件想要使用jQuery的方法,絲毫不難,只需要改變原型鏈指向即可,將自己的原型指向由原本指向init.prototype改為指向jQuery.prototype即可,結果:

淺析jQuery原理並仿寫封裝一個自己的庫

程式碼層面即一句話:

 jQuery.fn.init.prototype = jQuery.fn;複製程式碼

至此,我們的jQuery架構基本搭建完畢。此時,在沙箱外面,不需要手動new了,直接呼叫$(),例如$('div'),已經得到了和原版jQuery一樣的物件。

剩下的就是添磚加瓦,封裝一些方法了,我們以css方法為例,做個簡單的封裝。

完整程式碼如下:

(function(window, undefined) {

    var jQuery = function(selector) {  //jQuery是工廠函式
        return new jQuery.fn.init(selector); //傳入選擇器,例項化物件
    }  //引數selector會傳入init

    jQuery.fn = jQuery.prototype = {//工廠函式的原型   
        constructor: jQuery,
        init: function(selector) { //由jQuery一路傳來的形參 
            var ele = document.querySelectorAll(selector); //實現獲取物件。         
                 // this ==> init的例項物件    
                 // 把獲取到的元素新增到init的例項物件上          
            Array.prototype.push.apply(this, ele);
        },

        css: function(name, value) {// 通過判斷引數的個數就能確定css方法要實現什麼功能          
            if (arguments.length === 2) {
                // 設定單個樣式 
               // 是把獲取到的所有元素都設定上這個樣式 
               // this ==>$("p");  偽陣列,有length屬性,是需要把偽陣列中每一項都設定上樣式 
               for (var i = 0; i < this.length; i++) {
                 this[i].style[name] = value; 
               }        
             }else if(arguments.length === 1){
                //說明是個物件 設定多個樣式 || 獲取樣式 
               if (typeof name === "object") { 
                   // 設定多個樣式 需要給獲取到的所有元素都設定上多個樣式
                  for(var i = 0; i < this.length; i++){ 
                       //this[i] ==> 每一個元素  
                      // 迴圈的是物件,是設定的樣式和樣式值
                      for(var k in name){  
                          this[i].style[k] = name[k]; 
                       }
                    }
             }else if(typeof name === "string"){ 
                   // 獲取樣式  注意點: 獲取第一個元素對應的值
                    // this ==>$("p") this[0]  ==> 獲取到的元素中的第一個元素 
                  // style 操作的是行內樣式 
                  //window.getComputedStyle(元素, null); 獲取在元素上其效果的樣式
                  // 返回值: 是一個物件
                    return window.getComputedStyle(this[0], null)[name]; 
                 }
              }
            return this;// 目的:實現鏈式程式設計
         }
     }
    // 修改init的原型物件 目的是為了讓init的例項物件可以訪問jq上的方法
    jQuery.fn.init.prototype = jQuery.fn;
    window.jQuery = window.$ = jQuery;
})(window)複製程式碼

css方法的封裝略顯繁瑣,但css這玩意不一直都這樣麼,在js中處理css,吃力又難受。但也沒啥好辦法,還好這部分沒什麼難度。

但是有兩點仍然是需要我們注意並且必須做到的,就是關於JQ的兩個主要特點:

隱式迭代和鏈式程式設計。

前者使jq所設定的所有樣式都是對所有獲取到的物件都起作用,後者則要求在方法的封裝結尾,必須返回該物件,以供連續呼叫實現鏈式程式設計。如果封裝方法沒做到這兩點,那麼封裝出來的也就跟jq沒啥關係了,這是需要格外注意的。

【結語】本文是很久以前學習時寫的片段整理而成,限於個人水平,對jq封裝的思想可能理解的不夠深入,可能有許多地方說的不夠嚴謹或者似是而非,還請看官大佬們不吝指出。感謝。


相關文章