前言
业务开发中经常会用到异步函数,这里简单的对异步函数以及它的各种各样的解决方案做一个浅析
优缺点:
优点:
能够极大的提高程序并发业务逻辑的能力.
缺点:
异步函数的书写方式和代码执行逻辑很不直观,回调函数这种方式不太符合人类的的线性思维
异步函数的执行流程通常不好管理
不好对异步函数部署错误处理机制
解决方案
针对异步函数存在的缺点,所以才有了形形色色的异步的处理方案,常见的比如
原生的回调函数
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{}块,开始执行这个代码片段
一旦try{}块内部某一条代码没有正确执行,则不再执行try{}块内部的代码,而是立马跳出try{}块,同时会抛出一个异常,这个异常会被catch(){}捕获. 开始执行catch{}块里的代码.
我们假设code2出错了,整个函数内部的执行顺序是 code 0 -> code 1 -> code 2-> code 4 -> code 5;
-
如果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机制,错误处理非常非常容易.
结言
各种解决方案需要结合对应的业务场景使用