引言:2019年,react hooks成功上位,vue3.0發佈alpha版,TS使用率的飛速增加,以及大量前端開發工具使用體驗的大幅優化和提升等等讓愈來愈多的開發者吐槽前端學不動了的時候,最好的應對方式即是對基礎概念的掌握。內功足夠強大,才能作到不被別人牽着鼻子走。閱讀開源代碼是一個很好的方式,首先率選擇了jQuery即是裏面的內容沒有太多足夠抽象的設計思想。更多的是對於基礎內容的覆蓋。同時也包含一些不錯但設計模式在裏面,所以具備不錯的性價比。
jQuery是早期前端開發中佔比很重的一個庫。在手動操做DOM和瀏覽器差別較大的時代,jQuery經過統一和簡化不一樣瀏覽器之間的API,爲程序開發帶了極大的便利。因此jQuery的設計思路也是圍繞這兩點展開的。css
ps: 不作特殊說明,$
在源碼示例中等效jQuery
。前端
API設計的特色 ————> 函數重載vue
jQuery實際採用面向對象的方式進行程序開發。jQuery
自己是構造函數。react
jQuery('body').constructor === jQuery // true jQuery('body').addClass === jQuery.prototype.addClass // true // 由於 jQuery('body')的constructor和 addClass方法分別指向 jQuery自己和jQuery.prototype上的addClass方法, // 因此jQuery('body')返回的對象實際上就是jQuery構造函數生成的實例
可是在js中生成實例通常使用new
操做符,而jQuery通常的寫法是$()
。這裏實際上是經過某種技巧省略了new
操做符。首先有無new
,生成的實例都是等效的。jquery
(new jQuery('body')).constructor === jQuery('body').constructor // true (new jQuery('body')).__proto__ === jQuery('body').__proto__ // true // 這就證明了 有無new操做符,返回的結果是等效的。
這樣設計有一個好處是讓構造jQuery對象更加方便。ajax
那它的實現方式呢, 看一眼jQuery
函數的定義:編程
jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ); };
咱們發現jQuery方法返回的實際是 jQuery.fn.init
的實例。同時,咱們爲了讓生成的實例繼承jQuery.prototype上的方法,還須要添加一行代碼:json
jQuery.fn.init.prototype = jQuery.prototype;
關於js中構造函數和prototype的更多內容能夠查閱其它資料。segmentfault
關於new
操做符,咱們都知道在構造 函數沒有指定return對象的時候,會返回this
自己。若是咱們在無new
時,顯式指定return對象爲this(return this;
),是否是也等效於new呢?設計模式
答案: 不是。
這裏和函數中this的指向有關。一個函數或方法在執行時,內部的this指向分爲四個來源
已經肯定jQuery的開發採用面向對象的方式。而面向對象的兩個基本要素: 封裝和繼承。
封裝定義一個實例如何組裝完成,繼承定義多個實例間會共享的內容(行爲)。
$.fn.init
方法作了jQuery對象的封裝工做,經過一個簡單的$()
工廠函數調用。在init方法中,將一切可能的輸入源封裝爲jQuery對象。
有個須要特別說明的地方是,$()
方法除了接收普通的DOM對象或HTML字符串做爲輸入源返回一個jQuery對象外,還支持接收函數。這也是個語法糖,意思是在document ready的時候,調用這個函數。這麼沒有什麼特別的目的,就是爲了很是方便地定義一些在document ready執行的邏輯。由於在實際業務中,你的代碼執行的時候可能還有不少元素未加載。
jQuery對象的繼承基於js的原型對象完成。全部的jQuery對象都共享$.prototype對象上的方法。同時jQuery給自身添加了extend()
方法用於對象的擴展。那麼,也一樣能夠用於擴展自身的prototype對象,從而實現功能的擴展。這也是jQuery插件實現的基本原理。
須要預先明確的點: jQuery.prototype === jQuery.fn
。 這有什麼用? 手敲代碼的時候快一些。
jQuery中許多地方用到了鉤子思想,主要是用於處理瀏覽器的兼容性問題。在事件處理和css樣式設置中的體現尤其明顯。
事件處理包含綁定,分發和刪除三部分業務。jQuery中全部的事件(包括自定義事件)都會經過這三個方法進行處理。若是遇到自定義事件或者須要兼容性處理等特殊狀況,會經過jQuery.event.special處理。
jQuery.event.special實現的基礎是jQuery對瀏覽器的事件作了代理,全部在業務上須要綁定到元素事件的邏輯,最終都會交給一個統一的方法。這個方法經過原生API綁定到元素上,而後在事件被觸發時,此方法根據事件的上下文進行業務邏輯的分發。
jQuery對事件的綁定最終都收縮到jQuery.event.add
方法中,不論對外暴露的API是on()
或者one()
。 同時在模塊內部也有一個on
方法,這個方法一樣起到函數重載的做用,將參數處理成規範形式而後提交給jQuery.event.add
方法進行事件綁定操做。
jQuery.event.add
這個方法頗有意思,它並無直接把處理方法直接經過原生綁定方法綁定處理事件到元素上面。而是將EventHandler做爲數據存到元素自己(存儲的實現參考Data.js),若是元素對同一類型綁定了多個事件,這些事件會以數組的形式存在。若是沒有把handler直接幫到元素的事件上面,那麼如何在事件觸發時,調起這些邏輯?實際上是綁定了一個調度器,這個調度器會在事件觸發時,將存儲元素自己的方法逐一取出執行。
這是對於瀏覽器支持的普通事件的處理方式,若是是自定義事件呢?
答案就是 jQuery.event.special。
假如在執行自定義事件customEvent
綁定的邏輯時,jQuery首先檢查jQuery.event.special.customEvent
是否存在。若是存在的話,會走jQuery.event.special.customEvent
中定義的邏輯。 這個對象通常包含四個方法: setup
, add
, teardown
, 'remove'。做用於事件處理中不一樣生命週期。經過special對事件處理邏輯作攔截,在此基礎上能夠實現對原生事件行爲的重寫或者添加自定義事件。
若是不使用special,那麼如何處理兼容性問題。if...else ? 寫出來的邏輯成了麪條式的代碼。在事件處理中,包含三個基本要素: 綁定,解綁和分發。針對同一個事件兼容性處理,可能須要在這三個處理方法中分別添加兼容性業務的處理。這樣一來寫出來的邏輯必然十分繁雜。若是咱們以事件爲單位,定義各自的三種邏輯,而後交給程序在合適的時間調起。這樣一來,業務會清晰不少。
setup: 給該元素第一次綁定該事件時調用;
teardown: 給該元素解綁該事件最後一個handler時調用;
add: 給該元素添加handler;
remove: 給該元素移除handler;
handler: 當dispatch該事件的時候調用;
_default: 給該事件添加默認行爲;
若是setup/teardown 返回false,那麼會執行jQuery的bind/unbind方法(經過DOM native API)
關於css樣式相關方法的hooks是以jQuery.cssHooks
存在的,分爲 get
和 set
。
$.ajax容許接收dataType:jsonp
,可是咱們知道jsonp
是經過<script>
腳本實現的跨域請求,它不能經過XMLHttpRequest發送。那麼$.ajax有什麼特別的處理麼?
prefilters和transports。
這也是ajax能夠自定義dataType的關鍵點,原理跟event.special 相似。
jQuery本身實現了一個Callbacks方法,用於管理回調,主要是爲了提供給本身的defferred、ajax和animation使用。
實現基於觀察者模式,對外暴露 add,remove,fire這幾個API方法。 除了這三個方法,是沒法在外部直接修改回調list和執行狀態firing等數據的,經過閉包來實現。
同時提供了回調函數上下文的設置接口(fireWith)。
jQuery的設計思路就是找到頁面上的一些元素並執行一些操做。其中負責「找」的即是selector。而這一部分最終成爲一個獨立項目Sizzle。
Sizzle做爲查找器引擎,基於函數式編程的思路進行開發。基本的思路是將輸入(selector字符串)轉化爲輸出結果(與selector match的元素),不對輸入數據作任何變動,經過不一樣的輸入數據生成不一樣的函數而後執行最終函數得到目標數據。
Sizzle在轉換selector的中間過程當中,還對生成的函數進行緩存,進而在下次遇到相同的輸入時,能夠直接返回以前已經生成過的函數,從而得到性能的提高。
Sizzle自己實現了一個小型的compilor。爲何這麼說,在早先瀏覽器不支持querySelector/querySelectorAll
的時代,想想':first', " p ~ p"等之類的元素查找。這種寫法暗含了上下文相關。傳統的getElementsByTagName
方法必然包含了大量的回溯操做。這對於開發者是極爲不便利的,jQuery封裝了這些操做。這可能也是爲何當時能夠快速流行併成爲js中最流行的庫的緣由。
在經過查找引擎Sizzle找到目標元素後,就能夠對元素執行一些操做。
在jQuery中,咱們都知道進行DOM操做能夠採用鏈式寫法,好比像下面這樣對document.body進行操做:
$('body').addClass('foo').find('div').remove().end().addClass('bar')
那麼若是不採用鏈式寫法呢,會有什麼樣的結果,看下面
$('body').addClass('foo'); $('body').find('div').remove(); $('body').addClass('bar');
因此,一目瞭然~~~
這樣在進行DOM操做時,手寫代碼帶來的便利性是顯而易見的。實現這種寫法的機制也很簡單,就是在每一次操做以後,都返回對象自身return this
。
可是,若是某個方法須要返回操做結果或者其它數據,那麼這時候鏈式操做就沒法知足了。
jQuery中存在許多函數重載。咱們知道函數重載是在函數名相同的前提下,根據參數類型或個數來區分不一樣的處理。那麼函數重載在jQuery中有什麼意義呢?
$('body').css('width') $('body').css('width', '800px') $('body').css({ 'color': 'red', 'border': '10px solid blue' })
很明顯,css()是方法是被重載的函數。那若是不對css進行重載呢,想像一下,若是實現上面的功能應該怎麼設計程序。可能須要設計get/set方法或者針對每種參數類型都寫一個方法。那麼對外暴露的API就不只僅是一個css()了。API的繁可能是會增長使用者的學習成本的。
可是函數重載也不是隻好不壞,增長了程序的複雜性。在jQuery中,存在一些單純的normalize參數的方法。這樣讓開發者沒法第一眼就知道最終調用的是哪一個方法。這是對開發者而言,對於計算機,函數重載也可能會增長程序的消費。
關於函數重載,在jQuery的event處理中,獲得了更明顯的使用。綁定實踐最終會調用jQuery.event.add方法,可是在這以前,會先走on()方法,這個方法主要的做用就是規範函數的參數。
函數重載最終的效果的經過參數個數或者參數類型,類區分不一樣的處理方案, 減小了對外暴露的API數量。可是函數重載的基礎是在不一樣方案之間概念相近的狀況下,才建議採用函數重載。這樣對於使用者而言,也是清晰明確的。若是你把jQuery的find的方法重載到jQuery.css中,那誰能夠一眼看出find方法在哪一個API中呢。
舉幾個例子:
style()
方法;css()
方法;animate()
方法;domManip()
方法;那有個問題:何時該收,何時該放? 答:找到業務中的關鍵節點,而後在關鍵節點上作好覆蓋面比較廣的把控。
動畫都會以隊列的形式執行,默認隊列是fx
,那麼fx是如何實現的? 一個隊列應該具備自執行的特色,將處理方法以數組的形式存儲,而後再執行出棧時,給每一個方法添加一個鉤子,鉤住下一個要執行的方法,在執行完後調用下一個方法。
這個模式跟compose很像,將多個函數合併成一個函數執行。Koa和Redux的核心概念實現即是基於函數的compose。
jQuery從2005年發行至今(2019年12月),仍然在生產環境中佔據一席之地的緣由?
運行時負載是如今React/Vue等框架隨着業務功能的逐漸強大,也難以免,最終總會有一個天花板存在。也所以,有人搞出了無運行時負載的框架Svelte。Vue3更是強調本身的運行時性能是2.x的一倍,一部分提高得益於用Proxy替換了Object.defineProperty,另外一部分則是靜態編譯時作的性能優化。因此對於框架或者庫的設計,這也是應該考慮的一方面問題。
jQuery.event實現的基本原理demo special-events
jquery-edge-new-special-event-hooks
還需繼續努力~~
完!!!
本文參與了 SegmentFault思否徵文「2019 總結」,歡迎正在閱讀的你也加入。