前言
在js領域,promise出現的時間已經很久了,從jquery的$.get().done().fail() 這樣的API開始,到現在的es6默認支持的new Promise(),它的出現無疑使異步代碼變得信任可靠,更使得前端代碼的信任度直線提升,就跟它的名字一樣,它的到來,是一個 Promise (保證,允諾)
異步信任問題
有一個朋友在銀行系統工作,有一次,我幫他review一段代碼,據他所說,這段代碼給他帶來了一個很大的麻煩,有一位顧客使用這個頁面付款的時候意外的提交了兩次付款請求,boss復查發現提交的源頭是前端,當我接到這段代碼的時候,這位朋友已經焦頭爛額,我接過了代碼, 初步分析,什么也沒發現,他的代碼就像下面這樣:
$("btn").on("click",function(){
if($(this).hasClass("no-submit")){
$(this).removeClass("no-submit");
//忽略
$.ajax({
url:"....",
type:"json",
success:function(){
submit();
}
})
}
})
他告訴我,他已經針對按鈕做點擊唯一提交了,怎么可能調用兩次?
但是事實就是有兩次submit。
通過分析代碼,最后排查的結果,原因是這個jquery的$.ajax是被二次封裝過的,某種情況下回調函數被調用了兩次。
oh,看起來很簡單的原因,這可是個巨坑! 日常代碼中,我們都需要接觸各種各樣的第三方庫,誰來保證它們都沒有隱藏的問題?如果你遇到了經過加密混淆的庫,因為它內部某個try catch,或者內部某個if在非正常情況下發成的誤判導致重復調用,我想你就不會再淡定的喝着咖啡敲代碼了。
或者你覺得,防止重復調用這個很簡單,加個判斷就行了:
var a = 0; $("btn").on("click",function(){ if($(this).hasClass("no-submit")){ $(this).removeClass("no-submit"); //忽略 $.ajax({ url:"....", type:"json", success:function(){ if( a==0){ submit(); } } }) } })
完美是吧,測試上線,繼續喝咖啡。
三個月后。。。。。
有些顧客反應,點擊提交后,有時沒有提交成功,老板這時大發雷霆,這已經是第二次出現這樣的失誤了。
是的,只調用一次,但是鬼知道它會不會調用一次?
這里問題的本質是什么?
它就是所謂的回調地域。
我們總是有很多的回調函數需要交給第三方工具處理,類似於$.ajax,jquery就一定是值得信任的?圖樣圖森破!你永遠不知道jquery下面可能還埋着上一位開發者給你准備的周年大禮包。最糟糕的是,這個第三方工具你沒有修改的能力,要么你換掉他,要么你解密,要么換版本,誰知道作者會不會繼續更新?
把我們的回調交給不受信任的第三方程序,這就是回調地域,可怕的很!
Promise調用方式
今天的主角是promise。它可以讓我們的回調變得promise。
試想,我們把上述的ajax再次封裝(在不能改動源代碼的情況下),可能代碼如下:
var then = function(resolve,reject,timeout){ var a = 0; $("btn").on("click",function(){ if($(this).hasClass("no-submit")){ $(this).removeClass("no-submit"); //忽略 $.ajax({ url:"....", type:"json", success:function(){ a==0&&resolve(); a++; }, error:function(){ a==0&&reject(); a++; } }) } }) if(new Date.now()>timeout){ a==0&&reject(); a++; } } var a = 0;
ok,我只是將前面的代碼封裝到一個叫then的函數里,這個函數需要三個參數,resolve(解決),reject(拒絕),以及一個超時時間,另外控制調用次數的變量a也有保留。
有什么不同?
不同之處在於,then函數是可信任的,至少現在我們可以確定,它要么被resolve,要么reject,絕不可能出現調用兩次,或者不調用的情況。這種封裝可以叫做控制反轉,
我們將原本給到$.ajax的回調權再次交回自己手中,上面的代碼調用起來是這樣的:
then(function(){ submit() },function(){ error() })
ok,我們只解決了其中的一些問題,重復調用,不調用,但還有更多的問題。
讓promise登場吧!
使用promise封裝上面代碼:
function request(){ return new Promise(function(resolve,reject){ $.ajax({ url:"....", type:"json", success:function(){ resolve() }, error:function(){ reject() } }) }) } request.then(function(){ submit(); },function(){ error(); })
ok,是否豁然開朗?
promise就是解決關於異步回調信任,順便緩解了回調金字塔(一堆回調嵌套在一起,就是一個回調金字塔)問題的解決方案。
在這個Promise構造函數中傳入一個函數,這個函數有兩個值,resolve、reject。一旦代碼請求成功,resolve被調用,決議完成並忽略后面的所有調用。或者失敗后決議被拒絕,調用reject。
決議、解決、拒絕術語
一個promise通常有三種狀態 pending(進行中)、fulfilled(已成功)和rejected(已失敗)。
可以說在任何情況下,一個promise只有一個決議。 它只可能是fulfilled或者rejected其中一種,並且是不可以逆轉的。意思是你無法讓一個已經被拒絕的promise回滾。它永遠都會有一個確定的回復。
Promise API
promise.all(Array);
Promise.all方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例,並且這個新的promise返回的最終值是傳入的多個Promise決議完成的值。
有了Promise.all方法,就可以將異步操作以同步操作的流程表達出來,避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操作更加簡單易懂。
像這樣的調用:
Promise.all([request("url1"),request("url2")]).then((result)=>{
console.log(result) //[url1result,url2result]
})
注意傳入的順序,數組promise決議的值與返回的result數組位置是一致的。對於以往的回調方式來說,這是很強大的api。
Promise.race(array)
這個方法和all調用方式一樣,但是決議的方式不一樣,.all等待所有決議完成新的 Promise返回決議。 而.race,一旦有某個promise決議完成,其余的會立刻終止。也就是說它只會執行最快的那個promise。
結束
promise已經成為主流。我在這里只講了一些淺見,至於更多的用法,與君共勉!
