引子
相信各位developers對js中的異步概念不會陌生,異步操作后的邏輯由回調函數來執行,回調函數(callback function)顧名思義就是“回頭調用的函數”,函數體事先已定義好,在未來的某個時候由某個事件觸發調用,而這個時機,是程序本身無法控制的。
舉幾個常見例子:
-
事件綁定
-
動畫
-
Ajax
上面的例子簡單、典型,易於閱讀和理解。
為了引出本文的主題,假設現在有3個ajax異步操作,分別為A、B、C,每個都封裝成了函數,並可傳入success回調作為參數。
請考慮以下場景:
-
希望這3個異步操作按順序執行,形成執行隊列,即A執行完了,B執行;B執行完了,C執行;
-
希望A,B完成了,再執行C;
對於場景(1),代碼大概會是這樣:
按傳統的寫法,自然而然會形成回調函數的多層嵌套,如果代碼量比較多的話,代碼的可讀性和維護性會比較差。
對於場景(2),代碼可能要復雜些了:
-
a) A、B操作分別需要有狀態變量
isADone
,isBDone
,默認值均為false
;A成功后isADone
置為true
,B成功后isBDone
置為true
; -
b) 需要一段代碼來反復探測上述2個狀態變量,我們把這個行為稱為pending,等pending到2個狀態都為
true
時則執行C,並終止pending;
就本場景而言,上述方案還可勉強接受,如果情況再復雜些,代碼的可讀性和維護性也會大打折扣。
以上2種場景,只限定了3個異步操作的執行關系,實際的開發場景可能要比這個復雜的多,為了開發者能夠更優雅的處理js異步操作,jq引入了Deferred
對象($.Deferred
),我們先來了解下Deferred
對象,然后看看它能為我們的異步編程帶來哪些益處。
Deferred特性介紹
先創建一個Deferred
實例。
new
關鍵字是可選的,可以不寫,這是因為在Deferred
構造函數中處理了,但不代表用原生的js基於構造函數創建對象不用寫new
。
deferred
帶有3種狀態:pending(待定)、resolved(成功)、rejected(失敗)。
deferred
的狀態可以通過api進行切換,但不可逆。
可從英文字面意思理解:resolve(解決)后成功,reject(拒絕)后失敗。
狀態不可逆,是指一旦從待定狀態切換到任何一個確定狀態后,再次調用resolve
或reject
對原狀態將不起任何作用。
deferred
通過語義對應的api來綁定不同狀態時執行的回調函數。
resolve: done, always
reject: fail, always
其中always
綁定的回調函數,不論deferred
的狀態是成功或失敗,總會執行。
deferred
還有一個then
方法,來簡化done
、fail
的寫法:
另外,值得注意的是,當deferred
狀態切換后(調用reject
或者resolve
后),再進行回調函數的綁定,那么對應狀態的回調函數會立即執行。
舉個例子:
deferred
還可以返回自己的操作子集。
promise
只有綁定回調的api,而沒有狀態切換的api。
deferred: reject, resolve, done, fail, then, always
promise: done, fail, then, always
很明顯promise
是用來開放給外部調用的,而deferred
通常用於模塊內部,來控制自身狀態的切換。
舉個直觀的例子,一般在js中我們會用setTimeout
來做延時執行,如:
我們用deferred
來封裝下,代碼如下:
wait
函數內部,通過deferred
來進行狀態切換,返回promise
對象,這樣就可以在wait
外部進行回調函數的綁定。
其實該原理在jq中有個非常典型的案例,那就是$.ajax
方法,該方法會返回一個promise
,而方法內部有個deferred
用於決定自身狀態,所以相對傳統的ajax寫法,我們也可以這么寫:
其中done
,fail
等方法還可以接受多個callback、或者以callback array作為參數。
假定有個ajax請求,成功后需要對返回結果進行3個獨立的處理,分別對應3個函數,我們可以這么寫:
這樣獲取數據的邏輯和處理數據的邏輯進行了分離,數據處理不會全都堆在success回調函數中,代碼整體看起來就更簡潔易讀。
回到之前的問題
再了解了Deferred
特性和簡單應用后,我們再回頭考慮前面提到的2個場景,是否能夠用更好的方案來實現呢?答案是肯定的。
我們先看場景(2):A、B完成了,再執行C。
為什么先看場景(2),因為jq中正好有個現成的api:$.when
,能夠很方便的滿足該需求。
$.when
可接受多個deferred(promise)
對象,$.when
會返回一個promise,用作后續回調的綁定,官網示例如下:
當$.when
中2個異步請求均返回成功后,即會執行myFunc回調;
當$.when
中只要有一個異步請求失敗,即會執行myFailure回調;
解決方案已經很清晰了,我們稍加改造之前的asyncA
、asyncB
方法,讓它們分別返回各自的promise
,然后直接使用$.when
即可。
我可以先用ajax來做代碼示例:
下面再來個直接使用deferred
的代碼實例:
整體上,是否感覺代碼更清晰,更利於理解了呢?
我們再來看場景(1):A執行完了,B執行;B執行完了,C執行。
要完成這個實現的改造,需要深入了解下deferred
對象的then
方法。上面我們介紹了then
的一般用法:
用於done
,fail
的簡化寫法。但它還有一個非常重要的作用,可以用來傳遞deferred(promise)
對象,實現任務鏈條(chain tasks)。
下面就用then
來實現場景(1)的需求:
是不是感覺很簡單,如果C后面還有D,那繼續往下then
就可以了。
假定asyncA
是一個ajax操作,其中回調函數data參數,即為ajax成功后,后台返回的數據。
值得注意的是,如果用then
方法進行deferred
對象的傳遞,回調函數必須return一個deferred
。
上述的例子還有一個特點:由於then
的第一個參數是deferred
對象成功時執行的回調,若deferred
狀態切換到失敗,則后續then
的成功回調將不再執行,任務鏈就中斷了。
該場景有一定的實踐價值,比如一個業務網站,頁面上有多個展示模塊都是通過ajax問后台拿數據的,如果頁面一進來,同時向后台發好幾個ajax請求,會瞬間增加后台IO的壓力,可能會增加用戶等待界面反饋數據的時間,造成體驗下降。在這種情況下,把ajax請求作為隊列處理是比較合適的,可按重要性逐步請求獲取數據,提高性能和渲染體驗。
小結
本文只是初步的介紹了下jq中的Deferred
,並沒有深入到每一個細節,但它的基本功用應該是覆蓋到了,感興趣的同學可以前往jq官網,自行研究下。
其實promise
就可以按照字面理解為“承諾”,可以把deferred
理解成你的一個手下,你讓他去跟進一件事,而這個事情什么時候可以有個結果,你並不清楚,但他會給你承諾,將按照你的意圖:事情若這樣,后續要做什么;事情若那樣,后續要做什么。