【深入淺出jQuery】源碼淺析2--奇技淫巧


最近一直在研讀 jQuery 源碼,初看源碼一頭霧水毫無頭緒,真正靜下心來細看寫的真是精妙,讓你感嘆代碼之美。

其結構明晰,高內聚、低耦合,兼具優秀的性能與便利的擴展性,在瀏覽器的兼容性(功能缺陷、漸進增強)優雅的處理能力以及 Ajax 等方面周到而強大的定制功能無不令人驚嘆。

另外,閱讀源碼讓我接觸到了大量底層的知識。對原生JS 、框架設計、代碼優化有了全新的認識,接下來將會寫一系列關於 jQuery 解析的文章。

我在 github 上關於 jQuery 源碼的全文注解,感興趣的可以圍觀一下。jQuery v1.10.2 源碼注解 

 

系列第一篇:【深入淺出jQuery】源碼淺析--整體架構

本篇是系列第二篇,標題起得有點大,希望內容對得起這個標題,這篇文章主要總結一下在 jQuery 中一些十分討巧的 coding 方式,將會由淺及深,可能會有一些基礎,但是我希望全面一點,對看文章的人都有所幫助,源碼我還一直在閱讀,也會不斷的更新本文。

即便你不想去閱讀源碼,看看下面的總結,我想對提高編程能力,轉換思維方式都大有裨益,廢話少說,進入正題。

 

   短路表達式 與 多重短路表達式

短路表達式這個應該人所皆知了。在 jQuery 中,大量的使用了短路表達式與多重短路表達式。

短路表達式:作為"&&"和"||"操作符的操作數表達式,這些表達式在進行求值時,只要最終的結果已經可以確定是真或假,求值過程便告終止,這稱之為短路求值。這是這兩個操作符的一個重要屬性。

// ||短路表達式
var foo = a || b;
// 相當於
if(a){
	foo = a;
}else{
	foo = b;
}

// &&短路表達式
var bar = a && b;
// 相當於
if(a){
	bar = b;
}else{
	bar = a;
}

當然,上面兩個例子是短路表達式最簡單是情況,多數情況下,jQuery 是這樣使用它們的:

// 選自 jQuery 源碼中的 Sizzle 部分
function siblingCheck(a, b) {
	var cur = b && a,
		diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
		(~b.sourceIndex || MAX_NEGATIVE) -
		(~a.sourceIndex || MAX_NEGATIVE);

	// other code ...	
}

嗯,可以看到,diff 的值經歷了多重短路表達式配合一些全等判斷才得出,這種代碼很優雅,但是可讀性下降了很多,使用的時候權衡一下,多重短路表達式和簡單短路表達式其實一樣,只需要先把后面的當成一個整體,依次推進,得出最終值。

var a = 1, b = 0, c = 3;

var foo = a && b && c, // 0 ,相當於 a && (b && c) 
  bar = a || b || c;  // 1

這里需要提出一些值得注意的點:

1、在 Javascript 的邏輯運算中,0、""、null、false、undefined、NaN 都會判定為 false ,而其他都為 true ;

2、因為 Javascript 的內置弱類型域 (weak-typing domain),所以對嚴格的輸入驗證這一點不太在意,即便使用 && 或者 || 運算符的運算數不是布爾值,仍然可以將它看作布爾運算。雖然如此,還是建議如下:

if(foo){ ... }     //不夠嚴謹
if(!!foo){ ... }   //更為嚴謹,!!可將其他類型的值轉換為boolean類型

注重細節,JavaScript 既不弱也不低等,我們只是需要更努力一點工作以使我們的代碼變得真正健壯。

 

   預定義常用方法的入口

在 jQuery 的頭幾十行,有這么一段有趣的代碼:

(function(window, undefined) {
	var
		// 定義了一個對象變量,一個字符串變量,一個數組變量 
		class2type = {},
		core_version = "1.10.2",
		core_deletedIds = [],

		// 保存了對象、字符串、數組的一些常用方法 concat push 等等...
		core_concat = core_deletedIds.concat,
		core_push = core_deletedIds.push,
		core_slice = core_deletedIds.slice,
		core_indexOf = core_deletedIds.indexOf,
		core_toString = class2type.toString,
		core_hasOwn = class2type.hasOwnProperty,
		core_trim = core_version.trim;
		
})(window);

不得不說,jQuery 在細節上做的真的很好,這里首先定義了一個對象變量、一個字符串變量、數組變量,要注意這 3 個變量本身在下文是有自己的用途的(可以看到,jQuery 作者惜字如金,真的是去壓榨每一個變量的作用,使其作用最大化);其次,借用這三個變量,再定義些常用的核心方法,從上往下是數組的 concat、push 、slice 、indexOf 方法,對象的 toString 、hasOwnProperty 方法以及字符串的 trim 方法,core_xxxx 這幾個變量事先存儲好了這些常用方法的入口,如果下文行文當中需要調用這些方法,將會:

jQuery.fn = jQuery.prototype = {
	// ...
	
	// 將 jQuery 對象轉換成數組類型 
	toArray: function() {
		// 調用數組的 slice 方法,使用預先定義好了的 core_slice ,節省查找內存地址時間,提高效率
		// 相當於 return Array.prototype.slice.call(this)
		return core_slice.call(this);
	}
}

可以看到,當需要使用這些預先定義好的方法,只需要借助 call 或者 apply(戳我詳解)進行調用。

那么 jQuery 為什么要這樣做呢,我覺得:

1、以數組對象的 concat 方法為例,如果不預先定義好 core_concat = core_deletedIds.concat 而是調用實例 arr 的方法 concat 時,首先需要辨別當前實例 arr 的類型是 Array,在內存空間中尋找 Array 的 concat 內存入口,把當前對象 arr 的指針和其他參數壓入棧,跳轉到 concat 地址開始執行,而當保存了 concat 方法的入口 core_concat 時,完全就可以省去前面兩個步驟,從而提升一些性能;

2、另外一點,借助 call 或者 apply 的方式調用,讓一些類數組可以直接調用數組的方法。就如上面是示例,jQuery 對象是類數組類型,可以直接調用數組的 slice 方法轉換為數組類型。又譬如,將參數 arguments 轉換為數組類型:

function test(a,b,c){
	// 將參數 arguments 轉換為數組
	// 使之可以調用數組成員方法
	var arr = Array.prototype.slice.call(arguments);

	...
}

 

   鈎子機制(hook)

在 jQuery 2.0.0 之前的版本,對兼容性做了大量的處理,正是這樣才讓廣大開發人員能夠忽略不同瀏覽器的不同特性的專注於業務本身的邏輯。而其中,鈎子機制在瀏覽器兼容方面起了十分巨大的作用。

鈎子是編程慣用的一種手法,用來解決一種或多種特殊情況的處理。

簡單來說,鈎子就是適配器原理,或者說是表驅動原理,我們預先定義了一些鈎子,在正常的代碼邏輯中使用鈎子去適配一些特殊的屬性,樣式或事件,這樣可以讓我們少寫很多 else if 語句。

如果還是很難懂,看一個簡單的例子,舉例說明 hook 到底如何使用:

現在考公務員,要么靠實力,要么靠關系,但領導肯定也不會弄的那么明顯,一般都是暗箱操作,這個場景用鈎子實現再合理不過了。

// 如果不用鈎子的情況
// 考生分數以及父親名
function examinee(name, score, fatherName) {
    return {
        name: name,
        score: score,
        fatherName: fatherName
    };
}
 
// 審閱考生們
function judge(examinees) {
    var result = {};
    for (var i in examinees) {
        var curExaminee = examinees[i];
        var ret = curExaminee.score;
        // 判斷是否有后門關系
        if (curExaminee.fatherName === 'xijingping') {
            ret += 1000;
        } else if (curExaminee.fatherName === 'ligang') {
            ret += 100;
        } else if (curExaminee.fatherName === 'pengdehuai') {
            ret += 50;
        }
        result[curExaminee.name] = ret;
    }
    return result;
}
 
 
var lihao = examinee("lihao", 10, 'ligang');
var xida = examinee('xida', 8, 'xijinping');
var peng = examinee('peng', 60, 'pengdehuai');
var liaoxiaofeng = examinee('liaoxiaofeng', 100, 'liaodaniu');
 
var result = judge([lihao, xida, peng, liaoxiaofeng]);
 
// 根據分數選取前三名
for (var name in result) {
    console.log("name:" + name);
    console.log("score:" + score);
}

可以看到,在中間審閱考生這個函數中,運用了很多 else if 來判斷是否考生有后門關系,如果現在業務場景發生變化,又多了幾名考生,那么 else if 勢必越來越復雜,往后維護代碼也將越來越麻煩,成本很大,那么這個時候如果使用鈎子機制,該如何做呢?

// relationHook 是個鈎子函數,用於得到關系得分
var relationHook = {
    "xijinping": 1000,    
    "ligang": 100,
    "pengdehuai": 50,
   // 新的考生只需要在鈎子里添加關系分
}

// 考生分數以及父親名
function examinee(name, score, fatherName) {
    return {
        name: name,
        score: score,
        fatherName: fatherName
    };
}
 
// 審閱考生們
function judge(examinees) {
    var result = {};
    for (var i in examinees) {
        var curExaminee = examinees[i];
        var ret = curExaminee.score;
        if (relationHook[curExaminee.fatherName] ) {
            ret += relationHook[curExaminee.fatherName] ;
        }
        result[curExaminee.name] = ret;
    }
    return result;
}
 
 
var lihao = examinee("lihao", 10, 'ligang');
var xida = examinee('xida', 8, 'xijinping');
var peng = examinee('peng', 60, 'pengdehuai');
var liaoxiaofeng = examinee('liaoxiaofeng', 100, 'liaodaniu');
 
var result = judge([lihao, xida, peng, liaoxiaofeng]);
 
// 根據分數選取前三名
for (var name in result) {
    console.log("name:" + name);
    console.log("score:" + score);
}

可以看到,使用鈎子去處理特殊情況,可以讓代碼的邏輯更加清晰,省去大量的條件判斷,上面的鈎子機制的實現方式,采用的就是表驅動方式,就是我們事先預定好一張表(俗稱打表),用這張表去適配特殊情況。當然 jQuery 的 hook 是一種更為抽象的概念,在不同場景可以用不同方式實現。

看看 jQuery 里的表驅動 hook 實現,$.type 方法:

(function(window, undefined) {
	var 
		// 用於預存儲一張類型表用於 hook
		class2type = {};

	// 原生的 typeof 方法並不能區分出一個變量它是 Array 、RegExp 等 object 類型,jQuery 為了擴展 typeof 的表達力,因此有了 $.type 方法
	// 針對一些特殊的對象(例如 null,Array,RegExp)也進行精准的類型判斷
	// 運用了鈎子機制,判斷類型前,將常見類型打表,先存於一個 Hash 表 class2type 里邊
	jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) {
		class2type["[object " + name + "]"] = name.toLowerCase();
	});	

	jQuery.extend({
		// 確定JavaScript 對象的類型
		// 這個方法的關鍵之處在於 class2type[core_toString.call(obj)]
		// 可以使得 typeof obj 為 "object" 類型的得到更進一步的精確判斷
		type: function(obj) {

			if (obj == null) {
				return String(obj);
			}
			// 利用事先存好的 hash 表 class2type 作精准判斷
			// 這里因為 hook 的存在,省去了大量的 else if 判斷
			return typeof obj === "object" || typeof obj === "function" ?
				class2type[core_toString.call(obj)] || "object" :
				typeof obj;
		}
	})
})(window);

這里的 hook 只是 jQuery 大量使用鈎子的冰山一角,在對 DOM 元素的操作一塊,attr 、val 、prop 、css 方法大量運用了鈎子,用於兼容 IE 系列下的一些怪異行為。在遇到鈎子函數的時候,要結合具體情境具體分析,這些鈎子相對於表驅動而言更加復雜,它們的結構大體如下,只要記住鈎子的核心原則,保持代碼整體邏輯的流暢性,在特殊的情境下去處理一些特殊的情況:

var someHook = {
    get: function(elem) {
        // obtain and return a value
        return "something";
    },
    set: function(elem, value) {
        // do something with value
    }
}

從某種程度上講,鈎子是一系列被設計為以你自己的代碼來處理自定義值的回調函數。有了鈎子,你可以將差不多任何東西保持在可控范圍內。

從設計模式的角度而言,這種鈎子運用了策略模式。

策略模式:將不變的部分和變化的部分隔開是每個設計模式的主題,而策略模式則是將算法的使用與算法的實現分離開來的典型代表。使用策略模式重構代碼,可以消除程序中大片的條件分支語句。在實際開發中,我們通常會把算法的含義擴散開來,使策略模式也可以用來封裝一系列的“業務規則”。只要這些業務規則指向的目標一致,並且可以被替換使用,我們就可以使用策略模式來封裝他們。

策略模式的優點:

  • 策略模式利用組合,委托和多態等技術思想,可以有效的避免多重條件選擇語句;
  • 策略模式提供了對開放-封閉原則的完美支持,將算法封裝在獨立的函數中,使得它們易於切換,易於理解,易於擴展。
  • 策略模式中的算法也可以復用在系統的其它地方,從而避免許多重復的復制粘貼工作。

 

   連貫接口

無論 jQuery 如今的流行趨勢是否在下降,它用起來確實讓人大呼過癮,這很大程度歸功於它的鏈式調用,接口的連貫性及易記性。很多人將連貫接口看成鏈式調用,這並不全面,我覺得連貫接口包含了鏈式調用且代表更多。而 jQuery 無疑是連貫接口的佼佼者。

1、鏈式調用:鏈式調用的主要思想就是使代碼盡可能流暢易讀,從而可以更快地被理解。有了鏈式調用,我們可以將代碼組織為類似語句的片段,增強可讀性的同時減少干擾。(鏈式調用的具體實現上一章有詳細講到

// 傳統寫法
var elem = document.getElementById("foobar");
elem.style.background = "red";
elem.style.color = "green";
elem.addEventListener('click', function(event) {
  alert("hello world!");
}, true);

// jQuery 寫法
$('xxx')
	.css("background", "red")
	.css("color", "green")
	.on("click", function(event) {
  	  alert("hello world");
	});

2、命令查詢同體:這個上一章也講過了,就是函數重載。正常而言,應該是命令查詢分離(Command and Query Separation,CQS),是源於命令式編程的一個概念。那些改變對象的狀態(內部的值)的函數稱為命令,而那些檢索值的函數稱為查詢。原則上,查詢函數返回數據,命令函數返回狀態,各司其職。而 jQuery 將 getter 和 setter 方法壓縮到單一方法中創建了一個連貫的接口,使得代碼暴露更少的方法,但卻以更少的代碼實現同樣的目標。

3、參數映射及處理:jQuery 的接口連貫性還體現在了對參數的兼容處理上,方法如何接收數據比讓它們具有可鏈性更為重要。雖然方法的鏈式調用是非常普遍的,你可以很容易地在你的代碼中實現,但是處理參數卻不同,使用者可能傳入各種奇怪的參數類型,而 jQuery 作者想的真的很周到,考慮了用戶的多種使用場景,提供了多種對參數的處理。

// 傳入鍵值對
jQuery("#some-selector")
  .css("background", "red")
  .css("color", "white")
  .css("font-weight", "bold")
  .css("padding", 10);

// 傳入 JSON 對象
jQuery("#some-selector").css({
  "background" : "red",
  "color" : "white",
  "font-weight" : "bold",
  "padding" : 10
});

jQuery 的 on() 方法可以注冊事件處理器。和 CSS() 一樣它也可以接收一組映射格式的事件,但更進一步地,它允許單一處理器可以被多個事件注冊:

// binding events by passing a map
jQuery("#some-selector").on({
  "click" : myClickHandler,
  "keyup" : myKeyupHandler,
  "change" : myChangeHandler
});

// binding a handler to multiple events:
jQuery("#some-selector").on("click keyup change", myEventHandler);

 

   無 new 構造

怎么訪問 jQuery 類原型上的屬性與方法,怎么做到做到既能隔離作用域還能使用 jQuery 原型對象的作用域呢?重點在於這一句:

// Give the init function the jQuery prototype for later instantiation
jQuery.fn.init.prototype = jQuery.fn;

這里的關鍵就是通過原型傳遞解決問題,這一塊上一章也講過了,看過可以跳過了,將文字搬過來。

嘿,回想一下使用 jQuery 的時候,實例化一個 jQuery 對象的方法:

// 無 new 構造
$('#test').text('Test');

// 當然也可以使用 new
var test = new $('#test');
test.text('Test');

大部分人使用 jQuery 的時候都是使用第一種無 new 的構造方式,直接 $('') 進行構造,這也是 jQuery 十分便捷的一個地方。當我們使用第一種無 new 構造方式的時候,其本質就是相當於 new jQuery(),那么在 jQuery 內部是如何實現的呢?看看:

(function(window, undefined) {
	var 
	// ...
	jQuery = function(selector, context) {
		// The jQuery object is actually just the init constructor 'enhanced'
		// 看這里,實例化方法 jQuery() 實際上是調用了其拓展的原型方法 jQuery.fn.init
		return new jQuery.fn.init(selector, context, rootjQuery);
	},

	// jQuery.prototype 即是 jQuery 的原型,掛載在上面的方法,即可讓所有生成的 jQuery 對象使用
	jQuery.fn = jQuery.prototype = {
		// 實例化化方法,這個方法可以稱作 jQuery 對象構造器
		init: function(selector, context, rootjQuery) {
			// ... 
		}
	}
	// 這一句很關鍵,也很繞
	// jQuery 沒有使用 new 運算符將 jQuery 實例化,而是直接調用其函數
	// 要實現這樣,那么 jQuery 就要看成一個類,且返回一個正確的實例
	// 且實例還要能正確訪問 jQuery 類原型上的屬性與方法
	// jQuery 的方式是通過原型傳遞解決問題,把 jQuery 的原型傳遞給jQuery.prototype.init.prototype
	// 所以通過這個方法生成的實例 this 所指向的仍然是 jQuery.fn,所以能正確訪問 jQuery 類原型上的屬性與方法
	jQuery.fn.init.prototype = jQuery.fn;

})(window);

大部分人初看 jQuery.fn.init.prototype = jQuery.fn 這一句都會被卡主,很是不解。但是這句真的算是 jQuery 的絕妙之處。理解這幾句很重要,分點解析一下:

1)首先要明確,使用 $('xxx') 這種實例化方式,其內部調用的是 return new jQuery.fn.init(selector, context, rootjQuery) 這一句話,也就是構造實例是交給了 jQuery.fn.init() 方法取完成。

2)將 jQuery.fn.init 的 prototype 屬性設置為 jQuery.fn,那么使用 new jQuery.fn.init() 生成的對象的原型對象就是 jQuery.fn ,所以掛載到 jQuery.fn 上面的函數就相當於掛載到 jQuery.fn.init() 生成的 jQuery 對象上,所有使用 new jQuery.fn.init() 生成的對象也能夠訪問到 jQuery.fn 上的所有原型方法。

3)也就是實例化方法存在這么一個關系鏈  

  • jQuery.fn.init.prototype = jQuery.fn = jQuery.prototype ;
  • new jQuery.fn.init() 相當於 new jQuery() ;
  • jQuery() 返回的是 new jQuery.fn.init(),而 var obj = new jQuery(),所以這 2 者是相當的,所以我們可以無 new 實例化 jQuery 對象。

 

   setTimeout in Jquery

寫到這里,發現上文的主題有些飄忽,接近於寫成了 如何寫出更好的 Javascript 代碼,下面介紹一些 jQuery 中我覺得很棒的小技巧。

熟悉 jQuery 的人都知道 DOM Ready 事件,傳Javascript原生的 window.onload 事件是在頁面所有的資源都加載完畢后觸發的。如果頁面上有大圖片等資源響應緩慢, 會導致 window.onload 事件遲遲無法觸發,所以出現了DOM Ready 事件。此事件在 DOM 文檔結構准備完畢后觸發,即在資源加載前觸發。另外我們需要在 DOM 准備完畢后,再修改DOM結構,比如添加DOM元素等。而為了完美實現 DOM Ready 事件,兼容各瀏覽器及低版本IE(針對高級的瀏覽器,可以使用 DOMContentLoaded 事件,省時省力),在 jQuery.ready() 方法里,運用了 setTimeout() 方法的一個特性, 在 setTimeout 中觸發的函數, 一定是在 DOM 准備完畢后觸發。

jQuery.extend({
	ready: function(wait) {
		// 如果需要等待,holdReady()的時候,把hold住的次數減1,如果還沒到達0,說明還需要繼續hold住,return掉
		// 如果不需要等待,判斷是否已經Ready過了,如果已經ready過了,就不需要處理了。異步隊列里邊的done的回調都會執行了
		if (wait === true ? --jQuery.readyWait : jQuery.isReady) {
			return;
		}

		// 確定 body 存在
		if (!document.body) {
			// 如果 body 還不存在 ,DOMContentLoaded 未完成,此時
			// 將 jQuery.ready 放入定時器 setTimeout 中
			// 不帶時間參數的 setTimeout(a) 相當於 setTimeout(a,0)
			// 但是這里並不是立即觸發 jQuery.ready
			// 由於 javascript 的單線程的異步模式 
			// setTimeout(jQuery.ready) 會等到重繪完成才執行代碼,也就是 DOMContentLoaded 之后才執行 jQuery.ready
			// 所以這里有個小技巧:在 setTimeout 中觸發的函數, 一定會在 DOM 准備完畢后觸發
			return setTimeout(jQuery.ready);
		}

		// Remember that the DOM is ready
		// 記錄 DOM ready 已經完成
		jQuery.isReady = true;

		// If a normal DOM Ready event fired, decrement, and wait if need be
		// wait 為 false 表示ready事情未觸發過,否則 return
		if (wait !== true && --jQuery.readyWait > 0) {
			return;
		}

		// If there are functions bound, to execute
		// 調用異步隊列,然后派發成功事件出去(最后使用done接收,把上下文切換成document,默認第一個參數是jQuery。
		readyList.resolveWith(document, [jQuery]);

		// Trigger any bound ready events
		// 最后jQuery還可以觸發自己的ready事件
		// 例如:
		//    $(document).on('ready', fn2);
		//    $(document).ready(fn1);
		// 這里的fn1會先執行,自己的ready事件綁定的fn2回調后執行
		if (jQuery.fn.trigger) {
			jQuery(document).trigger("ready").off("ready");
		}
	}
})

 

暫且寫這么多吧,技巧還有很多,諸如 $.Deferred() 異步隊列的實現,jQuery 事件流機制等,篇幅較長,將會在以后慢慢詳述。

原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。

如果本文對你有幫助,請點下推薦,寫文章不容易。

系列第一篇:【深入淺出jQuery】源碼淺析--整體架構

最后,我在 github 上關於 jQuery 源碼的全文注解,感興趣的可以圍觀一下,給顆星星。jQuery v1.10.2 源碼注解 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM