大家好,这回我们来聊聊异步编程中的回调地狱问题。在上次的分析中,我们知道异步的本质就是回调。如果是简单的异步操作,通过回调就可以解决问题。如果需要对异步的状态进行管理,推荐使用Promise。但是,如果异步之间的关系,是一种层级递进的关系,那么当层级越来越深,便会出现传说中的Callback Hell(回调地狱)。就如同下图的景象一样,除了层级有神似以外,还有一个共同点就是让程序员面对这复杂的汉堡代码感到恐惧。
我们来假设一种场景,当A完成时去做B完成时去做C完成时去做D完成时去做E,其中ABCDE分别为四种异步操作。老样子我们用setTimeout来模拟异步操作,代码如下:
// 这就是传说中的Thunk函数,本质就是延迟求值,而这也是FP经常用到的方法。
functiondelay(time,data) {
return(cb) => {
setTimeout(() => {
cb(data);
},time);
}
}
constA =delay(100,'A');
constB =delay(200,'B');
constC =delay(300,'C');
constD =delay(400,'D');
constE =delay(500,'E');
A((a) => {
B((b) => {
C((c) => {
D((d) => {
E((e) => {
console.info('result:',a + b + c + d + e);
});
});
});
});
});
看到上面的代码说实话,从美学上我觉得还挺漂亮的,起码它很对称是吧!但是,这样的代码逻辑着实让人头疼。问题主要有3:
1. 调用栈过深引起性能问题;
JS引擎关注的闭包深度越深,处理的速度就越慢。
2. 逻辑耦合极其严重;
如:对E来说,本质上ABCD的的闭包全部都暴露给它。这样E和ABCD可以说从逻辑上完全耦合在了一起,是极其容易引入错误的。特别当业务逻辑复杂的时候,出错的概率那是大大的。
3. 代码可单元测试性低,可以说几乎为0;
好,我们换一种Promise的写法,能改善吗?
functiondelay(time,data) {
return newPromise(resolve => (setTimeout(() => {
resolve(data);
},time)));
}
constA =delay(100,'A');
constB =delay(200,'B');
constC =delay(300,'C');
constD =delay(400,'D');
constE =delay(500,'E');
A.then(a => {
B.then(b => {
C.then(c => {
D.then(d => {
E.then(e => {
console.info('result: ',a + b + c + d + e);
});
});
});
});
});
额,恐怕不行!好吧,确实如果在前端开发中,很少会出现这种场景。因为,你不可能在前端按顺序去发起ABCDE四个fetch请求来获得业务数据。那样,客户会等烦了的。如果有这样的场景我建议使用GraphQL,以后会再做这方面的介绍。但是,如果是开发node.js的时候这样的情况太普遍了!为什么,以为作为一门后端语言,必然会有很多IO操作,比如去读表、读缓存、插入记录到库表中等等。那这个时候,你就会有很多异步代码要写。
那,让我们来考虑怎么解决这个问题?
我个人觉得,当我们遇到问题,除了去NPM扒模块外。可以,先自己思考如何实现,而出发点就是从语言支持与FP编程思维去考虑。我们都知道babel能够将很大一部分的ES6代码翻译成ES5,为什么?因为JS有足够的灵活度,所以,其实我们有很大的创作发挥空间。有位大神说过JS本身是一门不断进化的语言,从现在TC39的每年发布一个版本来说更是证明了这点。这也是JS社区不断壮大的原因。下面,我们还是将自己至于不存在co模块的世界里,但是是在ES6存在世界。
这里,我想提一点是最近的感想,如果你想关注最新的技术走向,那你一定要去关注TC39的草案,因为,那将是一切的起点与关键。因为,我们必然是先知道了一些语法要素,才能用它去创造新事物。当然,你可以发明自己的语法要素。
好的,进入正题:当我们翻阅ES6的规范时,有一个叫Generator映入眼帘,我们发现它有两个点:
1. next方法: 只有执行next()的时候,它才会进入下一个迭代值;
2. yield的结果值,是有下一个next传递进来的;
例子如下:
functionprint(title) {
leti =;
return(value) => console.info(`$[${++i}]:【${typeofvalue ==='object'? JSON.stringify(value) : value}】`)
}
constprintHello =print('hello');
constprintMain =print('next ');
function*hello() {
printHello('begin');
consthello =yield'你好,';
printHello('hello');
constmodification =yieldhello +'编程日课的';
printHello(modification);
constname =yieldmodification +'朋友们';
printHello(name);
constend =yieldname +'!';
printHello(end);
}
constresult =hello();
letA = result.next();
printMain(A);
letB = result.next(A.value);
printMain(B);
letC = result.next(B.value);
printMain(C);
letD = result.next(C.value);
printMain(D);
/*
hello[1]:【begin】
next [1]:【{"value":"你好,","done":false}】
hello[2]:【hello】
next [2]:【{"value":"你好,编程日课的","done":false}】
hello[3]:【你好,编程日课的】
next [3]:【{"value":"你好,编程日课的朋友们","done":false}】
hello[4]:【你好,编程日课的朋友们】
next [4]:【{"value":"你好,编程日课的朋友们!","done":false}】
*/
上面的代码是否让你找到了灵感呢?上面说的两个特性刚好可以解决我们面临的问题。
1. next方法========>解决ABCDE要按顺序执行的问题;
2. yield的结果值====>解决A->B->C->D->E的问题,“A->B”标识,A计算出结果后才能计算B,并且要把值传递给B。
那开动你的脑筋,想想如何利用这两点来解决回调地狱问题,好像还差点什么是吧!
对的,就是结果里
next [4]:【{"value":"你好,编程日课的朋友们!","done":false}】
done的标识,当这个为true时候,那么任务也就结束了。也就是所有的yield都完成了。
好我们来分析下上面两个结论如何得出来的:
1、如果next不执行,那么下一句yield是不会执行的。这是不是让我们可以保证了顺讯。
2、yield语句的结果是靠下一次next来给入值,而当你下一次next的时候,就会执行下一步操作了。这样是不是保证了,后一步操作可以取到前一步操作的值。
PS:关于next注意两点,第一,next是执行yield的关键,第二,next传入的值将作为上一句yield语句的结果。
我们可以通过next的返回值,来获取到yield后面的值然后根据,这个值去判断类型,如果是一个异步操作,只有当异步处理完成后,再去触发next把这一个异步操作的结果给计算出来。好上代码:
functiondelay(time,data) {
return newPromise(resolve => (setTimeout(() => {
resolve(data);
},time)));
}
constA =delay(100,'A');
constB =delay(200,'B');
constC =delay(300,'C');
constD =delay(400,'D');
constE =delay(500,'E');
function*taskGenerator() {
consta =yieldA;
constb =yieldB;
constc =yieldC;
constd =yieldD;
conste =yieldE;
console.info('result: ',a + b + c + d + e);
}
consttask =taskGenerator();
lettaskStep = task.next();
while( !taskStep.done ) {
const{ value } = taskStep;
if(valueinstanceofPromise) {
value.then(data => {
taskStep = task.next(data);
})
}
}
执行的时候发现,卡住了。查了代码发现,while那个地方死循环了。?while一开始结束条件是无法异步的!所以,想到异步,我们只能用回调了,但是因为我们需要迭代执行,所以我们首先想到的就是递归。改造一下代码:
functiondelay(time,data) {
return newPromise(resolve => (setTimeout(() => {
resolve(data);
},time)));
}
constA =delay(100,'A');
constB =delay(200,'B');
constC =delay(300,'C');
constD =delay(400,'D');
constE =delay(500,'E');
function*taskGenerator() {
consta =yieldA;
constb =yieldB;
constc =yieldC;
constd =yieldD;
conste =yieldE;
console.info('result: ',a + b + c + d + e);
}
consttask =taskGenerator();
functionrunTask(task,data) {
let{ value,done } = task.next(data);
if(done) {
return;
}
if(valueinstanceofPromise) {
value.then(data => {
runTask(task,data);
})
}
}
runTask(task);
到这里,我们实现了一个简单co模块了,考虑一下如果执行的结果有同步的值怎么办?很简单只要处理下这种类型的数据,然后递归就好了,改造代码:
functiondelay(time,data) {
return newPromise(resolve => (setTimeout(() => {
resolve(data);
},time)));
}
constA =delay(100,'A');
constB =delay(200,'B');
constC =delay(300,'C');
constD =delay(400,'D');
constE =delay(500,'E');
constF ='谢谢大家!';
function*taskGenerator() {
consta =yieldA;
constb =yieldB;
constc =yieldC;
constd =yieldD;
conste =yieldE;
constf =yieldF;
console.info('result: ',a + b + c + d + e);
console.info(f);
}
consttask =taskGenerator();
functionrunTask(task,data) {
let{ value,done } = task.next(data);
if(done) {
return;
}
if(valueinstanceofPromise) {
value.then(data => {
runTask(task,data);
})
}else{
runTask(task,value);
}
}
runTask(task);
当然,这里还没有完全完善。如果yield是一个函数呢?yield是另外一个generator呢?
那在co里面是怎么做的呢?
在1.1.0版本的时候,TJ大神是这么做的:
functionco(fn) {
vargen = fn();
vardone;
functionnext(err,res) {
varret;
// multiple args
if(arguments.length >2) {
res = [].slice.call(arguments,1);
}
// error
if(err) {
try{
ret = gen.throw(err);
}catch(e) {
if(!done)throwe;
returndone(e);
}
}
// ok
if(!err) {
try{
ret = gen.send(res);
}catch(e) {
if(!done)throwe;
returndone(e);
}
}
// done
if(ret.done) {
if(done) done(null,ret.value);
return;
}
// thunk
if('function'==typeofret.value) {
try{
ret.value(next);
}catch(e) {
setImmediate(function(){
next(e);
});
}
return;
}
// promise
if(ret.value&&'function'==typeofret.value.then) {
ret.value.then(function(value) {
next(null,value);
},next);
return;
}
// neither
next(newError('yield a function or promise'));
}
setImmediate(next);
return function(fn){
done = fn;
}
}
各种类型判断,注意这个版本使用了setImmediate这是只有node才有的API ,所以说一开始只支持node不支持浏览器。
到了3.0.0的时候代码变成了
functionco(fn) {
varisGenFun =isGeneratorFunction(fn);
return function(done) {
varctx =this;
// in toThunk() below we invoke co()
// with a generator, so optimize for
// this case
vargen = fn;
// we only need to parse the arguments
// if gen is a generator function.
if(isGenFun) {
varargs = slice.call(arguments),len = args.length;
varhasCallback = len &&'function'==typeofargs[len -1];
done = hasCallback ? args.pop() :error;
gen = fn.apply(this,args);
}else{
done = doneerror;
}
next();
// #92
// wrap the callback in a setImmediate
// so that any of its errors aren't caught by `co`
functionexit(err,res) {
setImmediate(done.bind(ctx,err,res));
}
functionnext(err,res) {
varret;
// multiple args
if(arguments.length >2) res = slice.call(arguments,1);
// error
if(err) {
try{
ret = gen.throw(err);
}catch(e) {
returnexit(e);
}
}
// ok
if(!err) {
try{
ret = gen.next(res);
}catch(e) {
returnexit(e);
}
}
// done
if(ret.done)returnexit(null,ret.value);
// normalize
ret.value=toThunk(ret.value,ctx);
// run
if('function'==typeofret.value) {
varcalled =false;
try{
ret.value.call(ctx,function(){
if(called)return;
called =true;
next.apply(ctx,arguments);
});
}catch(e) {
setImmediate(function(){
if(called)return;
called =true;
next(e);
});
}
return;
}
// invalid
next(newError('yield a function, promise, generator, array, or object'));
}
}
}
/**
* Convert `obj` into a normalized thunk.
*
*@paramobj
*@paramctx
*@return
*@apiprivate
*/
functiontoThunk(obj,ctx) {
if(Array.isArray(obj)) {
returnobjectToThunk.call(ctx,obj);
}
if(isGeneratorFunction(obj)) {
returnco(obj.call(ctx));
}
if(isGenerator(obj)) {
returnco(obj);
}
if(isPromise(obj)) {
returnpromiseToThunk(obj);
}
if('function'==typeofobj) {
returnobj;
}
if(isObject(obj) Array.isArray(obj)) {
returnobjectToThunk.call(ctx,obj);
}
returnobj;
}
无论是什么统一为toThunk。
/**
* Execute the generator function or a generator
* and return a promise.
*
*@paramfn
*@return
*@apipublic
*/
functionco(gen) {
varctx =this;
if(typeofgen ==='function') gen = gen.call(this);
returnonFulfilled();
/**
*@paramres
*@return
*@apiprivate
*/
functiononFulfilled(res) {
varret;
try{
ret = gen.next(res);
}catch(e) {
returnPromise.reject(e);
}
returnnext(ret);
}
/**
*@paramerr
*@return
*@apiprivate
*/
functiononRejected(err) {
varret;
try{
ret = gen.throw(err);
}catch(e) {
returnPromise.reject(e);
}
returnnext(ret);
}
/**
* Get the next value in the generator,
* return a promise.
*
*@paramret
*@return
*@apiprivate
*/
functionnext(ret) {
if(ret.done)returnPromise.resolve(ret.value);
varvalue =toPromise.call(ctx,ret.value);
if(value &&isPromise(value))returnvalue.then(onFulfilled,onRejected);
returnonRejected(newTypeError('You may only yield a function, promise, generator, array, or object, '
+'but the following object was passed: "'+ String(ret.value) +'"'));
}
}
/**
* Convert a `yield`ed value into a promise.
*
*@paramobj
*@return
*@apiprivate
*/
functiontoPromise(obj) {
if(!obj)returnobj;
if(isPromise(obj))returnobj;
if(isGeneratorFunction(obj)isGenerator(obj))returnco.call(this,obj);
if('function'==typeofobj)returnthunkToPromise.call(this,obj);
if(Array.isArray(obj))returnarrayToPromise.call(this,obj);
if(isObject(obj))returnobjectToPromise.call(this,obj);
returnobj;
}
而到了4.0.0版本以后,采用ES6规范统一变成了Promise。
最后,大家可以思考一个问题为什么4.0以前,进行处理都用到了setImmediate,而4.0.0以后就不需要了!这是JS另外一个知识点了!
领取专属 10元无门槛券
私享最新 技术干货