Callback、Promise、Generator、async/await对比

发布:elantion 日期:2018-07-11 阅读:1458 评论:0

想象一下,如果你要泡一壶茶,你是一边烧水、一边洗杯子、一边准备茶叶,还是说先等水开发再去洗杯子,最后才去准备茶叶。显然,前者更为节省时间,但前者会更容易犯错,例如我们可能会忘记关燃气、可能会拿错茶叶等等。那有什么办法可以既节省时间,又不至于混乱呢?下面列举常用的方法解决顺序/异步执行问题,讨论之间的优缺点和如何选择。

callback

在很早以前,解决JS异步执行问题,除了部分异类使用定时器之外,几乎都是用回调来处理。至到现在,由于回调的速度仍比Promise之类的新语法有更高速的执行速度,在速度要求极高的场景依然很受欢迎。而且,由于逻辑比较简单,兼容性好,在需求简单的场景更好发挥它的长处。唯一让人抓狂的地方就是语法看起来很混乱,一层套一层的,很容把人看晕,面对复杂的场景常常会让人抓狂。我们接着泡茶的场景,看看如何泡杯茶:

//煮开水
var boilWater = function (cb) {
    setTimeout(function(){
        cb('boiledWater');
    }, 10*60*1000); //10分钟
};
//洗杯子
var washGlass = function(cb) {
    setTimeout(function(){
        cb('cleanGlass');
    }, 2*60*1000); //2分钟
};
//准备茶叶
var prepareTeaLeaves = function(cb){
    setTimeout(fucntion(){
        cb('teaLeaves');
    }, 1*60*1000); //1分钟
};

这三个函数分开理解都很简单,但我们怎么把他们结合起来,泡一杯完整的茶呢?我们看一下同步执行的代码:

boilWater(function(boiledWater){
    washGlass(function(cleanGlass){
    prepareTeaLeaves(function(teaLeaves){
        console.log(boiledWater + cleanGlass + teaLeaves);
        });
    });
});

先煮开开水,然后洗干净杯子,再然后准备茶叶,最后合并三种材料。一共耗时:10 + 2 + 1 分钟,显然我们没必要等水煮开了才去洗杯子,10分钟完全可以做完这些事情,所以这结果是不合理的。我们来看异步执行的代码:

var result = [];
var combine = function(){
    if(result.length === 3){
        console.log(result);
    }
};
boilWater(function(boiledWater){
    result.push(boiledWater);
    combine();
});
washGlass(function(cleanGlass){
    result.push(cleanGlass);
    combine();
});
prepareTeaLeaves(function(teaLeaves){
    result.push(teaLeaves);
    combine();
});

上面异步代码里,我们借助了辅助函数combine,也用了一个外部变量result来暂存泡茶的材料。每个泡茶的步骤结尾都执行combine,当result满足三个材料时,就输出结果。虽然耗时从原来的13分钟,变成了合理的10分钟,结果是正确的,但还是有不足的地方。

不足

现实开发中,我们不知道需要材料的数量,3这个数量就需要经常改变。又或者可能需要提前准备些必要材料,例如要先买水壶,我们才能烧开水。而且,每个步骤最后都要执行辅助函数,显得既麻烦,又容易出错。所以,有没有更优雅一点的实现方式?

Promise

Promise是近年来比较热门的新语法,主要解决异步执行的问题。像上面的场景,当需要异步执行时,只需这样写:

Promise.all([
    boilWater(),
    washGlass(),
    prepareTeaLeaves()
]).then(function(result){
    console.log(result);
});

简单说明:三个步骤同时执行,都完成之后就执行then函数。是不是看起来清爽多了?先别高兴,如果要实现这种效果,三个步骤都需要改成Promise方式。

//煮开水
var boilWater = function () {
    return new Promise(function(resolve){
        setTimeout(function(){
            resolve('boiledWater');
        }, 10*60*1000); //10分钟
    });
};
//洗杯子
var washGlass = function() {
    return new Promise(function(resolve){
         setTimeout(function(){
             resolve('cleanGlass');
         }, 2*60*1000); //2分钟        
    });
};
//准备茶叶
var prepareTeaLeaves = function(){
    return new Promise(function(resolve){
        setTimeout(fucntion(){
            resolve('teaLeaves');
       }, 1*60*1000); //1分钟        
    });
};

看上去好像没什么不一样,只是把cb换成了resolve,但为什么每个函数都要返回一个Promise实例?因为Promise字面意思就是"承诺",这里把三个步骤都改成三个承诺,承诺给你开水,承诺给你杯子,承诺给你茶叶,最后Promise.all就是要求把三个承诺都兑现了,我们才执行"然后"then函数。
除此之外,还有Promise.race用于解决竟争返回的问题,例如同时烧两壶水,那壶先开就用那一壶,这就是典现的Promise.race应用场景。

不足

如同callback一样,同样会有嵌套问题,例如我们希望顺序执行时:

var result = [];
boilWater()
    .then(function(boiledWater){
        result.push(boiledWater);
        return washGlass();
    }).then(function(cleanGlass){
        if(cleanGlass === false){return false; }
        result.push(cleanGlass);
        return prepareTeaLeaves();
    }).then(function(teaLeaves){
        if(teaLeaves === false){return false; }
        result.push(teaLeaves);
        console.log(result);
    });

虽然不像callback套那么多层,但仍不怎么美观,而且当我们需要在某过程中需要停止执行(或者在中途返回了错误的值),还必须得层层判断后跳出,非常麻烦。

Generator

Generator函数是一种可以中途暂停、异步执行的函数,它的语法很简单:

function * gen(){
    //do something
}

回到泡茶的场景,当需要顺序执行时,代码就变得非常简单干净:

function * gen(){
    yield boilWater();
    yield washGlass();
    yield prepareTeaLeaves();
}
var makeTea = gen();
var boiledWater = makeTea.next();
var claenGlass = makeTea.next();
var teaLeaves = makeTea.next();

中途那里出错了,直接return就可以了,也不用Promise那样找一个暂存变量。但不足也很明显,就是每次都要执行next()显得很麻烦,虽然有co(第三方包)可以解决,但就多包了一层,不好看,错误也必须按co的逻辑来处理,不爽。

async/await

async/await可以说是最近的大热门技术点,因为它可以说完全解决了js各种奇奇怪怪的执行顺序问题,上面各种场景都非常适合async/await使用。例如接着上面顺序执行的问题:

var makeTea = async function (){
    var boiledWater = await boilWater(); //三个步骤函数都要使用Promise那节的函数
    var cleanGlass = await washGlass();
    var teaLeaves = await prepareTeaLeaves();
}

如果中途出错返回空值了,可以直接return中止执行。

//略
var boiledWater = await boilWater();
if(!boiledWater){return false; }
//略

是不是清爽了许多,代码变得相当容易理解。那如果要异步执行呢?看下面:

var result = await Promise.all([
    boilWater(),
    washGlass(),
    prepareTeaLeaves()
]);

Promise怎么又来了呢?其实async/await只是generator的语法糖,在并行执行方面还是Promise来得比较方便。从上面两个实例可以看到,各个函数都是扁平的,不会产生多余的嵌套,代码更加清爽易读。

总结

上面各个技术点都有他们各自的优缺点,如果你要我选一个未来的趋势,我会选择async/await,它确实解决了许多痛点,包括代码易读性,书写简易程度,中止,捕错,并行,顺行执行等都有很好的实现方式。但如果你的项目可能只有几十行代码,那真没什么必要用async/await,用callback就很好,因为async/await是ES8的语法,需要工具进行预编,例如babel,如果要兼容旧浏览器,那还要加载pollyfill文件,既笨重又麻烦。所以,大家要据根具体场景选用吧。