切圖崽的自我修養-[ES6] 非同步函式管理方案淺析

大切圖崽發表於2019-02-16

前言

業務開發中經常會用到非同步函式,這裡簡單的對非同步函式以及它的各種各樣的解決方案做一個淺析

優缺點:

優點:

  • 能夠極大的提高程式併發業務邏輯的能力.

缺點:

  • 非同步函式的書寫方式和程式碼執行邏輯很不直觀,回撥函式這種方式不太符合人類的的線性思維

  • 非同步函式的執行流程通常不好管理

  • 不好對非同步函式部署錯誤處理機制


解決方案

針對非同步函式存在的缺點,所以才有了形形色色的非同步的處理方案,常見的比如

  • 原生的回撥函式

  • promise/A+

  • async/await(generator);


業務場景

但這些解決方案各自能解決什麼問題,才是我們所關心的.
實際上,如果對業務場景進行抽象,開發過程中對非同步函式的管理可以抽象成如下的幾種需求
比如有非同步函式f1,f2,f3:

  • 對f1,f2,f3之間的執行順序沒有要求. 它們的執行結果不互相依賴,誰先完成誰後完成無關緊要

  • 對f1,f2,f3之間的執行順序沒有要求. 它們的執行結果不互相依賴,誰先完成誰後完成無關緊要. 但有一個函式f4,它必須等到f1,f2,f3執行完畢之後才能執行

  • 對f1,f2,f3之間的執行順序有要求,必須要滿足f1->f2->f3的執行順序

下面就來簡單介紹一下,各個解決方案針對不同的業務場景,能解決什麼問題


需求1

對f1,f2,f3執行完成的順序沒有要求,即它們的執行結果是不互相依賴的,我們可以寫成如下的形式

    f1(function(){});
    f2(function(){});
    f3(function(){});
    ...
    

需求2

f1,f2,f3之間執行完成的順序沒有要求,即它們各自的執行結果是不互相依賴的,但有一個函式f4,需要等f1,f2,f3函式全部執行完成之後才能執行

解決方法:`維護一個記數器`. f1,f2,f3的執行順序無關緊要,但對於f1,f2,f3每一個完成的回撥裡,都要判斷是否3個函式都已完成(通過count來判斷),如果都已完成,則執行f4.  Ps(這裡的寫成自執行的形式是防止count被汙染)  實際上,node的三方非同步管理模組EventProxy, 以及promise的promise.all的實現,都是採用這種方式來對非同步函式進行管理的.

    (function(){
        let count = 0;
        function handler(){
            if(count==3){
                f4();
            }
        }
        
        f1(function(){count++; handler();});
        f2(function(){count++; handler();});
        f3(function(){count++; handler();});
    }()

需求3

對於非同步函式f1,f2,f3,我想保證它們的執行順序是f1->f2->f3的順序(即f1如果執行成功,呼叫f2,如果f2執行成功,呼叫f3)

3.1

按最原始的方法,可以寫成如下回撥巢狀的形式.即把f2作為f1的回撥,f3作為f3的回撥.依次巢狀就可以滿足f1->f2->f3這種呼叫形式. 這種方法雖然能夠滿足需求但同時存在很多問題: 回撥層級太深不好除錯.

最簡單的情況,假設不考慮f1,f2,f3出錯的情況(即f1,f2,f3全部都執行正確),函式的執行流程大概是這樣:

    
    f1(function(){
        f2(function(){
            f3(function(){
                ...
            })
        })
    })

實際上,考慮到各個非同步函式都有可能出錯的分支, 真實的執行流程應該是這樣(這才三層回撥巢狀,程式碼已經完全混亂的不能看了):



    f1(function(){
        if(err){
            //f1 err handler
        }
        else{
            f2(function(){
                if(err){
                    //f2 err handler    
                }
                
                else{
                    f3(function(){
                        if(err){
                            //f2 err handler
                        }
                        else{
                            ...
                        }
                    })    
                }
            
            })
        }
    })
    
        

3.2

為了解決這個巢狀過深這種問題,所以有了promise這種的解決方案. 這種規則邏輯比較清晰,更容易理解,但需要做一點點預備工作. 即非同步函式f1,f2,f3全部要先封裝成promise規範,這裡拿f1舉例(f2,f3同理).

   function f1(){
           var promiseObj = new Promise(function(resolve,reject){
            //f1的具體功能程式碼實現
            ...
            
            if(f1err){ //如果f1執行出錯
                reject(failValue);
            }
            else{ //如果f1執行成功
                resolve(successValue);
            }
           })
           return promiseObj;
   }
   

預備工作做完了,我們來看具體實現


    f1()
    .then(function suc(){return f2()},function fail(){/*f1 err handler*/})
    .then(function suc(){return f3()},function fail(){/*f2 err handler*/})
    .then(function suc(){},function fail(){/*f3 err handler*/})

簡單來分析下,首先f1()執行完成後,會返回一個promise物件,它會被then捕獲,如果promise物件的狀態是resolve狀態,會呼叫then的第一個引數,即成功回撥. 如果promise物件的狀態是reject狀態,會呼叫then的第二個引數,即失敗回撥.

如果f1執行成功,則會在then中的成功回撥suc中呼叫f2(),而f2()返回的也是一個promise物件,會被下一個then捕獲…依次類推

如果f1執行失敗,會在then的失敗回撥fail中呼叫你寫的err handler控制程式碼,然後return跳出整個執行鏈就可以

我們可以看到promise的語法實際上是將深度巢狀的邏輯通過then的處理平攤了.在這種語法規則下,f1->f2->f3的執行順序一目瞭然.當然它還是有缺點的,就像之前提到的,它必須要做一些預備工作,即需要把非同步函式要封裝成promise規範. 另外,它還有一堆then,看起來有點頭暈

3.3

既然promise我們也覺得有點麻煩,那隻能試試es7的async/await了,聽說async/await+promise是管理非同步回撥的終極解決方案

首先來明晰下try/catch的概念. 當一個程式碼片段,我們不能確定它到底能不能成功執行的情況下,就會用try/catch處理. 當fun函式自上到下執行,一開始會進入try{}塊,開始執行這個程式碼片段

  1. 一旦try{}塊內部某一條程式碼沒有正確執行,則不再執行try{}塊內部的程式碼,而是立馬跳出try{}塊,同時會丟擲一個異常,這個異常會被catch(){}捕獲. 開始執行catch{}塊裡的程式碼. 我們假設code2出錯了,整個函式內部的執行順序是 code 0 -> code 1 -> code 2-> code 4 -> code 5;

  2. 如果try{}塊內部的程式碼片段全都正確執行了.就不會進入catch{}的錯誤處理流程了. 這時候整個函式內部的執行順序是 code 0 -> code 1 -> code 2-> code 3 -> code 5;

      
      
              functionfun(){
                
                    /* code 0 */
    
                
                    try{
                        /* code 1 */
                        /* code 2 */
                        /* code 3 */                
                    }
                    catch(err){
                        /* code 4 */
                    }
                    
                    /* code 5 */
    
                }
                
                fun();
                
                
    

對應到async上也是同理,async函式有一個特點,它的await能監聽一個promise物件. 如果監聽到的promise物件是resolve正確態,那麼await這條語句相當於是被正確執行了,不會進入catch{}流程. 但如果監聽到的promise是reject錯誤態,則會認為await語句執行失敗了,會丟擲異常然後跳進catch{}錯誤處理.

    var funa = function(){
        var promiseObj_a = new Promise(function(resolve,reject){
            setTimeout(function(){resolve(1);},1000);
        });
        return promiseObj_a;
    }
    var funb = function(){
        var promiseObj_b = new Promise(function(resolve,reject){
            setTimeout(function(){resolve(2);},5000)
        });
        return promiseObj_b;
    }
    var func = function(){
        var promiseObj_c = new Promise(function(resolve,reject){
            setTimeout(function(){reject(3);},8000);
        });
        return promiseObj_c;
    }
    
    async function testAsync(){
        
        try {
            var a =await funa();
            console.log(a,`resolve`);
        }
        catch(erra){
            console.log(erra,`reject`);
        }
        
        try {
            var b =await funb();
            console.log(b,`resolve`);
        }
        catch(errb){
            console.log(errb,`reject`);
        }
    
        try {
            var c =await func();
            console.log(c,`resolve`);
        }
        catch(errc){
            console.log(errc,`reject`);
        }
    }
    
    testAsync();
    //輸出結果是 
    //1 resolve
    //2 resolve
    //3 reject
    

我們能看到async/await配合promise帶來了巨大的好處. 首先非同步函式的執行順序能夠像同步一樣一眼看出來,簡單明瞭. 其次,針對任何一個非同步函式的執行,都有完善的try/catch機制,錯誤處理非常非常容易.

結言

各種解決方案需要結合對應的業務場景使用

相關文章