1. 程式人生 > >es6之promise被坑記

es6之promise被坑記

怒了,新浪微博也太坑了,程式碼片段太麻煩了。

接下來看怎麼被坑的。
先說說被坑的原因,主要是以前一直以為jQuery的deferred物件和promise是一樣的api,所以一直沒有深入研究,用jQuery也用習慣了,就一直放棄了promise的深入研究。

事實證明,淺嘗輒止是可恥的,也是最容易被坑的。

程式碼1如下。

    var error = true;
    function test(){
        //promise處理非同步函式的回撥,這裡簡單的用一個error代表正確結果和非預期結果的判斷條件
        var promise = new Promise(function
(resolve,reject){
setTimeout(function(){ if(!error){ resolve("沒錯"); }else{ reject("有錯"); } },100) }); //test函式內部處理非預期結果。 return promise.catch(function(error){ console.log("失敗了:"
+error); }); } test().then(function(str){ console.log("成功了:"+str); });

當error為false的時候,輸出結果

成功了:沒錯

當error為true的時候,輸出結果

失敗了:有錯
成功了:undefined

然後我就傻眼了,搞什麼啊,為什麼明明失敗了,成功的回撥還會被呼叫呢?
然後開始瘋狂google。

看到一個答案是:

當catch函式裡沒有丟擲新的error時,promise認為錯誤已經被解決。

天真的我以為找到了問題的根源,於是簡單的改成程式碼2

    var error = true;
    function test(){
        var promise = new Promise(function(resolve,reject){
            setTimeout(function(){
                if(!error){
                    resolve("沒錯");
                }else{
                    reject("有錯");
                }
            },100)
        });
        return promise.catch(function(error){
            //沒錯,就是改了這裡而已
            throw new Error("失敗了:"+error)
        });
    }
    test().then(function(str){
        console.log("成功了:"+str);
    });

然後重新執行,結果變成了

Uncaught (in promise) Error: 失敗了:有錯

這個 Uncaught是個什麼鬼?明明前面不是要丟擲異常麼?為什麼又來個沒有catch?
於是我開始懷疑我的寫法的問題。
又開始改成程式碼3:

    var error = true;
    function test(){
        var promise = new Promise(function(resolve,reject){
            setTimeout(function(){
                if(!error){
                    resolve("沒錯");
                }else{
                    reject("有錯");
                }
            },100)
        });
        promise.catch(function(error){
            throw new Error("失敗了:"+error)
        });
        return promise;
    }
    test().then(function(str){
        console.log("成功了:"+str);
    });

結果變成了:

Uncaught (in promise) Error: 失敗了:有錯
Uncaught (in promise) 有錯

什麼鬼?為什麼改成這種寫法以後又多出一個Uncaught。
此刻的我徹底驚呆了。開始懷疑自己的世界觀了,認為自己到底是有多不小心,肯定哪里哪里的寫法寫錯了,然後重複看上面的三篇基礎介紹裡的例子,對照自己的寫法,始終沒發現問題。

到了這一步的時候,我徹底凌亂了,一度覺得這個promise是什麼鬼東西,還不如jquery的deferred呢。手放在刪除鍵上猶豫了很久要不要改回jquery的deferred物件。

還好最後我忍住了衝動,重新認真的看了介紹。
總算明白了原因。

Promise是一種鏈式的回撥,每一個then或者catch返回的是一個已經處理過的promise物件,同時catch只能catch之前鏈條中出現的錯誤。

那結合上面的那個說明,也就是說,我的catch函式因為是寫在test函式內部的,所以它是這個鏈式回撥的第一個環節,它只能catch promise的executor裡的reject的分支,程式碼1中因為catch了reject分支,所以promise認為錯誤已經被處理了,自然會繼續呼叫then的回撥。
而程式碼2中,因為在catch中重新丟擲了錯誤,而在這個catch之後的then裡沒有做錯誤的處理,於是出現了Uncaught的錯誤提示。
在程式碼3中,改了寫法,return的是初始promise物件,所以實際上catch和then都屬於第一鏈,在catch中因為丟擲了新的error,故而出現一個Uncaught提示,而在then中沒有定義reject的callback,且在then之後沒有新增catch callback,所以會丟擲第二個Uncaught提示。

問題總算找到了。
於是正確程式碼應該是

    var error = true;
     function test(){
        var promise = new Promise(function(resolve,reject){
            setTimeout(function(){
                if(!error){
                    resolve("沒錯");
                }else{
                    reject("有錯");
                }
            },100)
        });
        return promise
    }
    test().then(function(str){
        console.log("成功了:"+str);
    }).catch(function(error){
        console.log("失敗了:"+error)
    });

也就是將程式碼1中的catch移到then的後面即可。執行結果也就成為了預期結果

失敗了:有錯

但是,問題是首先如果在之後再加入catch callback,根據前面的理論,下一個catch因為前面的reject已經被處理,所以第二個catch應該是不會執行的,那是不是意味著錯誤的回撥只能寫一次呢?
改成程式碼4

    var error = true;
    function test(){
        var promise = new Promise(function(resolve,reject){
            setTimeout(function(){
                if(!error){
                    resolve("沒錯");
                }else{
                    reject("有錯");
                }
            },100)
        });
        return promise
    }
    test().then(function(str){
        console.log("成功了:"+str);
    }).catch(function(error){
        console.log("失敗了:"+error)
    }).catch(function(error){
        console.log("第二個回撥:"+error);
    });

結果為:

失敗了:有錯

果然,第二個catch沒有執行。
那如果我在這個之後再加入一個then呢?會有什麼樣的結果?
程式碼5

失敗了:有錯
第二個成功回撥:有錯

至此已經基本明白promise的鏈式規則了。
1、每一個回撥都接受上一個響應過的回撥的返回值作為引數,程式碼5中,因為響應的是第一個catch callback,所以處理完錯誤後響應了第二個then的回撥,而第二個then的引數則為第一個catch callback的返回值。如果error為false的話,第二個then的引數則變成了第一個then的返回值。
2、catch函式只能catch在鏈條之前發生的reject,同時,瀏覽器的錯誤也會被認為是reject狀態,且reject的內容為錯誤提示。

這兩點jQuery的deferred是和promise的實現方法是不同的。
jQuery的deferred的fail函式是有錯誤發生的時候就會響應,無論寫在鏈式的什麼位置。且,每一個callback的引數都為最初的resolve或者reject的值。

現在,回到最初我想解決的問題,如果我想要在test函式裡統一處理reject,而test函式之外,只接受resolve的狀態怎麼實現?

要實現這個,其實也很簡單,只需要保證catch不是在then之後就可以了,也就是將程式碼2裡的throw部分改成處理錯誤既可。於是改成程式碼6

    var error = true;
    function test(){
        var promise = new Promise(function(resolve,reject){
            setTimeout(function(){
                if(!error){
                    resolve("沒錯");
                }else{
                    reject("有錯");
                }
            },100)
        });
        promise.catch(function(error){
            console.log("失敗了:"+error)
            return error;
        })
        return promise;
    }
    test().then(function(str){
        console.log("成功了:"+str);
    })

當我得意洋洋以為理解透徹了的時候,結果又讓我嚇了一跳

失敗了:有錯
Uncaught (in promise) 有錯

什麼鳥,為什麼還有Uncaught的提示?
冷靜想想,首先,Uncaught的意思是代表有reject狀態沒有被catch,那沒有被catch的地方只有可能是在test函式外的then後面。按照前面的理論,程式碼6的寫法裡,catch完以後我返回了原本的promise物件,而原本的promise物件裡reject的狀態其實是沒有catch的。所以才會出現Uncaught的提示。

這樣一來,好吧,只能丟棄jQuery的deferred的影響。
如果要在test裡處理錯誤,就只能在非同步的函式裡進行處理,也就是將setTimeout的reject去掉,只留下resolve,然後每一個then都處理resolve,並且返回直接將引數作為返回值。

但是這樣一來,在test外就沒有辦法再次呼叫錯誤回調了。

好吧,看來是我的要求比較繞,要求本身是和jQuery的deferred的功能是一致的。

但是,為了不使用jQuery同時又滿足我的需求,那就只能做一個比較繞的解決辦法,終極解決辦法就是