天天看點

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

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

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

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

我在 **thub 上關于 jQuery 源碼的全文注解,感興趣的可以圍觀一下。jQuery v1.10.2 源碼注解&*bsp;。

&*bsp;

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

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

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

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

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

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

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

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

1、在 *avascr*pt 的邏輯運算中,0、""、*ull、false、u*def**ed、NaN 都會判定為 false ,而其他都為 true ;

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

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

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

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

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

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

1、以數組對象的 co*cat 方法為例,如果不預先定義好&*bsp;core_co*cat = core_deletedIds.co*cat 而是調用執行個體 arr 的方法 co*cat 時,首先需要辨識目前執行個體 arr 的類型是 Array,在記憶體空間中尋找 Array 的 co*cat 記憶體入口,把目前對象 arr 的指針和其他參數壓入棧,跳轉到 co*cat 位址開始執行,而當儲存了 co*cat 方法的入口 core_co*cat 時,完全就可以省去前面兩個步驟,進而提升一些性能;

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

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

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

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

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

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

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

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

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

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

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

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

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

政策模式的優點:

政策模式利用組合,委托和多态等技術思想,可以有效的避免多重條件選擇語句;

政策模式提供了對開放-封閉原則的完美支援,将算法封裝在獨立的<code>函數</code>中,使得它們易于切換,易于了解,易于擴充。

政策模式中的算法也可以複用在系統的其它地方,進而避免許多重複的複制粘貼工作。

&amp;*bsp; &amp;*bsp;連貫接口

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

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

2、指令查詢同體:這個上一章也講過了,就是函數重載。正常而言,應該是指令查詢分離(Comma*d a*d Query Separat*o*,CQS),是源于指令式程式設計的一個概念。那些改變對象的狀态(内部的值)的函數稱為指令,而那些檢索值的函數稱為查詢。原則上,查詢函數傳回資料,指令函數傳回狀态,各司其職。而 jQuery 将 *etter 和 setter 方法壓縮到單一方法中建立了一個連貫的接口,使得代碼暴露更少的方法,但卻以更少的代碼實作同樣的目标。

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

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

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

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

嘿,回想一下使用 jQuery 的時候,執行個體化一個 jQuery 對象的方法:

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

大部分人初看&amp;*bsp;jQuery.f*.***t.prototype = jQuery.f* 這一句都會被卡主,很是不解。但是這句真的算是 jQuery 的絕妙之處。了解這幾句很重要,分點解析一下:

1)首先要明确,使用 $('xxx') 這種執行個體化方式,其内部調用的是&amp;*bsp;retur* *ew jQuery.f*.***t(selector, co*text, rootjQuery) 這一句話,也就是構造執行個體是交給了&amp;*bsp;jQuery.f*.***t() 方法取完成。

2)将 jQuery.f*.***t 的 prototype 屬性設定為 jQuery.f*,那麼使用 *ew jQuery.f*.***t() 生成的對象的原型對象就是 jQuery.f* ,是以挂載到 jQuery.f* 上面的函數就相當于挂載到 jQuery.f*.***t() 生成的 jQuery 對象上,所有使用&amp;*bsp;*ew jQuery.f*.***t()&amp;*bsp;生成的對象也能夠通路到&amp;*bsp;jQuery.f* 上的所有原型方法。

3)也就是執行個體化方法存在這麼一個關系鍊&amp;*bsp;&amp;*bsp;

jQuery.f*.***t.prototype = jQuery.f* = jQuery.prototype ;

*ew jQuery.f*.***t() 相當于 *ew jQuery() ;

jQuery() 傳回的是 *ew jQuery.f*.***t(),而 var obj = *ew jQuery(),是以這 2 者是相當的,是以我們可以無 *ew 執行個體化 jQuery 對象。

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

熟悉 jQuery 的人都知道 DOM Ready 事件,傳*avascr*pt原生的 w**dow.o*load 事件是在頁面所有的資源都加載完畢後觸發的。如果頁面上有大圖檔等資源響應緩慢, 會導緻 w**dow.o*load 事件遲遲無法觸發,是以出現了DOM Ready 事件。此事件在 DOM 文檔結構準備完畢後觸發,即在資源加載前觸發。另外我們需要在 DOM 準備完畢後,再修改DOM結構,比如添加DOM元素等。而為了完美實作 DOM Ready 事件,相容各浏覽器及低版本IE(針對進階的浏覽器,可以使用 DOMCo*te*tLoaded 事件,省時省力),在 jQuery.ready() 方法裡,運用了 setT*meout() 方法的一個特性,&amp;*bsp;在 setT*meout 中觸發的函數, 一定是在 DOM 準備完畢後觸發。

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

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

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

最後,我在 **thub 上關于 jQuery 源碼的全文注解,感興趣的可以圍觀一下,給顆星星。jQuery v1.10.2 源碼注解&amp;*bsp;。