淺析jQuery原理並仿寫封裝一個自己的庫
【前言】最近專案忙的腳不沾地,剛剛結束,準備整理一下以前寫的一些學習筆記和技術文章。本文原是很久之前看jq原始碼時寫的片段,隔了很久再看都忘得差不多了。簡單整理出來,做個記錄。
作 為一名前端工程師,jQuery是我們熟的不能再熟的工具之一,其強大的功能和近乎完美的相容封裝使其成為前端領域必備的技能。用了很長時間的jq,卻一直沒有去探索學習過jQuery原始碼,似乎不算是一名合格的前端er。最近趁著空閒研究了一下,jQuery原始碼可謂是深邃如海,奧妙無窮,平時工作中偶爾需要封裝工具,也不必像它這樣面面俱到,但是簡單學習其封裝思路還是很有意義的。後面還要抽時間仔細學習每一塊的原始碼。這裡簡單說一說我對jq框架封裝的理解,並仿照著封裝css的兩個方法。
首先,jQuery的本質是一個封裝了眾多方法的庫
這個庫的框架是一個沙箱
其最外層的框架或者說起骨架如下:
(function(window, undefined) { var jQuery = function () {} window.jQuery = window.$ = jQuery;//暴露全域性 })(window)複製程式碼
可以很明顯的看出其框架結構,在一個沙箱中,所有的函式、方法都放在沙箱內部,頂一個一個名為jQuery的函式,這是核心函式,所有的成員都圍繞它運轉。
為什麼要傳入 window
和 undefined
,原因很簡單,傳入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原型上的,難道還要手動搬回來?算了算了,太麻煩,還好有原型鏈這個好東西。手繪了一張草圖,將就看一下:
init的例項物件想要使用jQuery的方法,絲毫不難, 只需要改變原型鏈指向即可,將自己的原型指向由原本指向init.prototype改為指向jQuery.prototype即可 ,結果:
程式碼層面即一句話:
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封裝的思想可能理解的不夠深入,可能有許多地方說的不夠嚴謹或者似是而非,還請看官大佬們不吝指出。感謝。