切图崽的自我修养-[ES6] 异步函数管理方案浅析

Posted jlfw

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了切图崽的自我修养-[ES6] 异步函数管理方案浅析相关的知识,希望对你有一定的参考价值。

前言

业务开发中经常会用到异步函数,这里简单的对异步函数以及它的各种各样的解决方案做一个浅析

优缺点:

优点:

  • 能够极大的提高程序并发业务逻辑的能力.

缺点:

  • 异步函数的书写方式和代码执行逻辑很不直观,回调函数这种方式不太符合人类的的线性思维

  • 异步函数的执行流程通常不好管理

  • 不好对异步函数部署错误处理机制


解决方案

针对异步函数存在的缺点,所以才有了形形色色的异步的处理方案,常见的比如

  • 原生的回调函数

  • 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机制,错误处理非常非常容易.

结言

各种解决方案需要结合对应的业务场景使用

以上是关于切图崽的自我修养-[ES6] 异步函数管理方案浅析的主要内容,如果未能解决你的问题,请参考以下文章

切图崽的自我修养-[MVVM] Js MV*模式浅谈

1W字图解23种设计模式(TypeScript版)——前端切图崽必修内功心法

程序员自我修养阅读笔记——Linux共享库管理

程序员自我修养阅读笔记——Linux共享库管理

IT技术管理者的自我修养

Web | ES6的异步编程