jQuery 2.0.3 源碼分析 Deferred概念
JavaScript編程幾乎總是伴隨着異步操作,傳統的異步操作會在操作完成之後,使用回調函數傳回結果,而回調函數中則包含了後續的工作。這也是造成異步編程困難的主要原因:我們一直習慣於“線性”地編寫代碼邏輯,但是大量異步操作所帶來的回調函數,會把我們的算法分解地支離破碎。此時我們不能用if來實現邏輯分支,也不能用while/for/do來實現循環,更不用提異步操作之間的組合、錯誤處理以及取消操作了。因此也誕生了如jQuery Deferred這樣的輔助類庫
舉一個例子會有助於理解
我做的是phonegap項目,所以涉及到的異步處理就別特多:
1. 常見的setTimeout
2. 與底層代碼通信,調用插件cordova.exec
3. postmessage與iframe通信
4. CSS3 動畫
5. Ajax
6. HTML5的本地數據
等等…
我就拿HTML5 Web Database爲例:
數據庫
查詢語句
//數據庫鏈接對象 config.db = window.openDatabase(config.dbName, "1.0", "Xxtebook Database", config.dbSize); config.db.transaction(function(tx) { tx.executeSql('SELECT * FROM Novel'
, [], function(tx, results) { result = results.rows; console.log('result',result) // }); }, function(){}, function(){});
就這段代碼其實本身是沒問題的,但是這裏至少要涉及到異步方面的考慮:
javaScript要求在與服務器進行交互時要用異步通信,如同AJAX一樣,因爲是異步模型,所以在調用transaction遊覽器提供的本地數據接口時候類似AJAX(這裏我是假設),瀏覽器自己有內部的XHR方法異步處理,但是此時的JS代碼還是會同步往下執行,其實就是無阻塞的代碼
問題:因爲無無阻塞,代碼在發送AJAX這個請求後會繼續執行,那麼後續的操作如果依賴這個數據就會出錯了,所以這裏就需要等待AJAX返回,才能執行後續操作
我們換個更簡單 僞代碼
alert(1) setTimeout(function(){ alert(2) },0) alert(3) //alert(1) //alert(3) //alert(2)
代碼都能看懂,因爲異步而導致流程不正確
或者說我們的應用在某個程度上依賴第三方api的數據,那麼就會面臨一個共同的問題:
我們無法獲悉一個API響應的延遲時間,應用程序的其他部分可能會被阻塞,直到它返回 結果。Deferreds 對這個問題提供了一個更好的解決方案,它是非阻塞的,並且與代碼完全解耦 。
當然異步操作也可以提供一個類似成功回調,失敗回調的通知接口
JS是單線程語言,就簡單性而言,把每一件事情(包括GUI事件和渲染)都放在一個線程裏來處理是一個很好的程序模型,因爲這樣就無需再考慮線程同步這些複雜問題。
另一方面,他也暴露了應用開發中的一個嚴重問題,單線程環境看起來對用戶請求響應迅速,但是當線程忙於處理其它事情時,就不能對用戶的鼠標點擊和鍵盤操作做出響應。
What's The Point Of Promises?
http://www.kendoui.com/blogs/teamblog/posts/13-03-28/what-is-the-point-of-promises.aspx
http://wiki.commonjs.org/wiki/Promises/A
大多情況下,promise作爲一個模型,提供了一個在軟件工程中描述延時(或將來)概念的解決方案。
它背後的思想我們已經介紹過:不是執行一個方法然後阻塞應用程序等待結果返回,而是返回一個promise對象來滿足未來值。
這樣看來,Promise/A只是一種規範,Deferred可以看作這種規範的具體實現
畫了一個很簡短的圖,大家略過一下,後面有很詳細的流程圖
我們先看看規範如何定義的:
Promise/A提議’定義了一個’then‘方法來註冊回調,當處理函數返回結果時回調會執行。它返回一個promise的僞代碼看起來是這樣的:
promise = function create( //數據庫鏈接對象 config.db = window.openDatabase(config.dbName, "1.0", "Xxtebook Database", config.dbSize); config.db.transaction(function(tx) { tx.executeSql('SELECT * FROM Novel', [], function(tx, results) { result = results.rows; console.log('result',result) // }); }, function(){}, function(){}); ); promise.then(function( futureValue ) { }); promise.then(function( futureValue ) { });
promise回調會在處於以下兩種不同的狀態下執行:
- resolved:在這種情況下,數據是可用
- rejected:在這種情況下,出現了錯誤,沒有可用的值
'then'方法接受兩個參數:一個用於promise得到了解決(resolved),另一個用於promise拒絕(rejected)。讓我們回到僞代碼
promise.then( function( futureValue ) { //成功 } , function() { //失敗 } );
在某些情況下,我們需要獲得多個返回結果後,再繼續執行應用程序(例如,在用戶可以選擇他們感興趣的選項前,顯示一組動態的選項)。這種情況下,'when'方法可以用來解決所有的promise都滿足後才能繼續執行的場景。
when( promise1, promise2, ... ).then(function( futureValue1, futureValue2, ... ) { //當所有的異步對象都準備完畢後,才執行 });
我們在模擬一個場景
如果同時執行多個動畫的時候,我們就需要對每一個動畫執行完畢後的處理下一個動作,意味着我們就要監聽所有動畫的執行完畢後的回調
使用promise和‘when’方式卻可以很直截了當的表示: 一旦動畫執行完成,就可以執行下一步任務。最終的結果是我們可以可以簡單的用一個回調來解決多個動畫執行結果的等待問題
when( function(){ //執行動畫1 return promise }, function(){ //執行動畫2 return promise 2 } ).then(function(){ //當2個動畫完成後,我們在自行後續的代碼 });
基本上可以用非阻塞的邏輯方式編寫代碼並異步執行。 而不是直接將回調傳遞給函數,這可能會導致緊耦合的接口,通過promise模式可以很容易區分同步和異步的概念。
根據這個規範,我們看看JQ是如何實現的?
Deferred Object :http://api.jquery.com/category/deferred-object/
由於1.7版本後回調函數Callbacks從Deferred中抽離出去了,所以在此之前還需要了解下 回調函數-callback
本文主要是涉及源碼的實現
我們先對API有個整體的認識
jQuery.Deferred() |
一個構造函數,返回一個鏈式實用對象方法來註冊多個回調,回調隊列, 調用回調隊列,並轉達任何同步或異步函數的成功或失敗狀態。 |
deferred.always() |
當Deferred(延遲)對象解決或拒絕時,調用添加處理程序 |
deferred.done() |
當Deferred(延遲)對象解決時,調用添加處理程序 |
deferred.fail() |
當Deferred(延遲)對象拒絕時,調用添加處理程序。 |
deferred.isRejected() |
確定一個Deferred(延遲)對象是否已被拒絕。 |
deferred.isResolved() |
確定一個Deferred(延遲)對象是否已被解決。 |
deferred.notify() |
根據給定的 args參數 調用Deferred(延遲)對象上進行中的回調 (progressCallbacks) |
deferred.notifyWith() |
根據給定的上下文(context)和args遞延調用Deferred(延遲)對象上進行中的回調(progressCallbacks ) |
deferred.pipe() |
實用的方法來過濾 and/or 鏈Deferreds。 |
deferred.progress() |
當Deferred(延遲)對象生成進度通知時,調用添加處理程 |
deferred.promise() |
返回Deferred(延遲)的Promise(承諾)對象。 |
deferred.reject() |
拒絕Deferred(延遲)對象,並根據給定的args參數調用任何失敗回調函數(failCallbacks) |
deferred.rejectWith() |
拒絕Deferred(延遲)對象,並根據給定的 context和args參數調用任何失敗回調函數(failCallbacks)。 |
deferred.resolve() |
解決Deferred(延遲)對象,並根據給定的args參數調用任何完成回調函數(doneCallbacks) |
deferred.resolveWith() |
解決Deferred(延遲)對象,並根據給定的 context和args參數調用任何完成回調函數(doneCallbacks) |
deferred.state() |
確定一個Deferred(延遲)對象的當前狀態 |
deferred.then() |
當Deferred(延遲)對象解決,拒絕或仍在進行中時,調用添加處理程序。 |
jQuery.when() |
提供一種方法來執行一個或多個對象的回調函數, Deferred(延遲)對象通常表示異步事件。 |
.promise() |
返回一個 Promise 對象用來觀察當某種類型的所有行動綁定到集合,排隊與否還是已經完成 |
構建一個異步對象:
我們傳統的寫法
function aaa(callback){ setTimeout(function(){ callback( 5 ); },1000); } function done(value){ alert(value) } aaa(function(value){ done(value); })
換成JQuery的寫法
var defer = $.Deferred(); //構建異步對象 setTimeout(function(){ defer.resolve( 5 ); },1000); var filtered = defer.then(function( value ) { return value * 2; }); filtered.done(function( value ) { console.log('打印出值',value) });
通過對比我們還是能感覺到很大的不同:
傳統的代碼邏輯,通過嵌套回調函數,等待異步發送成功的通知後,在執行下一步操作,如果有大量的嵌套回調,耦合度,維護性,擴展性?
那麼Deferred對象存在的意義在哪裏?
從我角度來說,對於開發者
1:代碼可讀性,扁平化結構
2:讓支離破碎的代碼結構,繼續保存線性的代碼邏輯,也就是異步代碼轉爲同步
3: 從抽線的角度,提供了一個抽象的非阻塞的解決方案(如 Ajax 請求的響應),它創建一個 “promise” 對象,其目的是在未來某個時間點返回一個響應。
deferreds 可以理解爲表示需要長時間才能完成的耗時操作的一種方式,相比於阻塞式函數它們是異步的,而不是阻塞應用程序等待其完成然後返回結果。 deferred對 象會立即返回,然後你可以把回調函數綁定到deferred對象上,它們會在異步處理完成後被調用。
所以,總的來講Deferred通過一組 API 來規範化異步操作,這樣也能夠讓異步操作的流程控制更加容易
下章就是具體的源碼分析了,會有附帶流程圖及源碼的分析
類似的異步庫可以參考
http://www.cnblogs.com/aaronjs/p/3169328.html
http://www.cnblogs.com/aaronjs/p/3168588.html
基本都大同小異