延期(deferred)的承諾(promise) — jq異步編程淺析


引子

相信各位developers對js中的異步概念不會陌生,異步操作后的邏輯由回調函數來執行,回調函數(callback function)顧名思義就是“回頭調用的函數”,函數體事先已定義好,在未來的某個時候由某個事件觸發調用,而這個時機,是程序本身無法控制的。

舉幾個常見例子:

  • 事件綁定

  • 動畫

  • Ajax

上面的例子簡單、典型,易於閱讀和理解。

為了引出本文的主題,假設現在有3個ajax異步操作,分別為A、B、C,每個都封裝成了函數,並可傳入success回調作為參數。

請考慮以下場景:

  1. 希望這3個異步操作按順序執行,形成執行隊列,即A執行完了,B執行;B執行完了,C執行;

  2. 希望A,B完成了,再執行C;

對於場景(1),代碼大概會是這樣:

按傳統的寫法,自然而然會形成回調函數的多層嵌套,如果代碼量比較多的話,代碼的可讀性和維護性會比較差。

對於場景(2),代碼可能要復雜些了:

  • a) A、B操作分別需要有狀態變量isADoneisBDone,默認值均為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(拒絕)后失敗。

狀態不可逆,是指一旦從待定狀態切換到任何一個確定狀態后,再次調用resolvereject對原狀態將不起任何作用。

deferred通過語義對應的api來綁定不同狀態時執行的回調函數。

resolve: done, always

reject: fail, always

其中always綁定的回調函數,不論deferred的狀態是成功或失敗,總會執行。

deferred還有一個then方法,來簡化donefail的寫法:

另外,值得注意的是,當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寫法,我們也可以這么寫:

其中donefail等方法還可以接受多個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回調;

解決方案已經很清晰了,我們稍加改造之前的asyncAasyncB方法,讓它們分別返回各自的promise,然后直接使用$.when即可。

我可以先用ajax來做代碼示例:

下面再來個直接使用deferred的代碼實例:

整體上,是否感覺代碼更清晰,更利於理解了呢?

我們再來看場景(1):A執行完了,B執行;B執行完了,C執行。

要完成這個實現的改造,需要深入了解下deferred對象的then方法。上面我們介紹了then的一般用法:

用於donefail的簡化寫法。但它還有一個非常重要的作用,可以用來傳遞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理解成你的一個手下,你讓他去跟進一件事,而這個事情什么時候可以有個結果,你並不清楚,但他會給你承諾,將按照你的意圖:事情若這樣,后續要做什么;事情若那樣,后續要做什么。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM