【深入淺出jQuery】原始碼淺析2–奇技淫巧

發表於2016-03-23

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

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

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

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

 

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

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

 

   短路表示式 與 多重短路表示式

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

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

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

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

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

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

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

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

 

   預定義常用方法的入口

在 jQuery 的頭幾十行,有這麼一段有趣的程式碼:

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

可以看到,當需要使用這些預先定義好的方法,只需要藉助 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 轉換為陣列型別:

 

   鉤子機制(hook)

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

鉤子是程式設計慣用的一種手法,用來解決一種或多種特殊情況的處理。

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

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

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

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

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

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

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

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

 

   連貫介面

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

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

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

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

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

 

   無 new 構造

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

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

嘿,回想一下使用 jQuery 的時候,例項化一個 jQuery 物件的方法:

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

大部分人初看 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

寫到這裡,發現上文的主題有些飄忽,接近於寫成了 如何寫出更好的 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 準備完畢後觸發。

 

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

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

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

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

相關文章