co模块鉴赏分析-回调地狱

大家好,这回我们来聊聊异步编程中的回调地狱问题。在上次的分析中,我们知道异步的本质就是回调。如果是简单的异步操作,通过回调就可以解决问题。如果需要对异步的状态进行管理,推荐使用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另外一个知识点了!

  • 发表于:
  • 原文链接http://kuaibao.qq.com/s/20180201G0Y8SN00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券

年度创作总结 领取年终奖励