假设你有几个函数fn1、fn2、fn3需要依次调用。最简单的方法当然是:fn1();fn2();fn3();但有时这些函数是运行时添加的,调用时不知道有哪些函数;这时候我可以预先定义一个数组,在添加函数的时候把函数压入里面,需要的时候一个一个的从数组中取出,依次调用:varstack=[];//执行其他operations,definefn1stack.push(fn1);//执行其他操作,definefn2,fn3stack.push(fn2,fn3);//当调用stack.forEach(function(fn){fn()});这样函数有没有名字都无所谓,直接传入匿名函数也可以。让我们测试一下:varstack=[];functionfn1(){console.log('firstcall');}stack.push(fn1);functionfn2(){console.log('secondcall');}stack.push(fn2,function(){console.log('thirdcall')});stack.forEach(function(fn){fn()});//output'firstinorder'Firstcall','Secondcall','Thirdcall'这个实现到目前为止工作正常,但是我们忽略了一种情况,就是异步函数的调用。异步是JavaScript中绕不开的话题,这里不打算讨论JavaScript中异步相关的各种术语和概念,欢迎广大读者查阅(如某著名评论)。如果你知道下面的代码会输出1,3,2,请继续阅读:console.log(1);setTimeout(function(){console.log(2);},0);console.log(3);如果栈队列中的函数是类似的异步函数,我们的实现就会乱七八糟:varstack=[];functionfn1(){console.log('firstcall')};stack.push(fn1);functionfn2(){setTimeout(functionfn2Timeout(){console.log('secondcall');},0);}stack.push(fn2,function(){console.log('secondcall');Threecalls')});stack.forEach(函数(fn){fn()});//输出'firstcall','thirdcall','secondcall'问题很明显,fn2确实是按顺序调用的,但是setTimeout中的函数fn2Timeout(){console.log('secondcall')}不是立即执行(即使超时设置为0);fn2调用后立即返回,然后执行fn3后,fn3执行完毕,才真正轮到fn2Timeout。如何处理?我们分析一下,这里的关键是fn2Timeout。我们必须等到它真正执行后才能调用fn3。理想情况下,它看起来像这样:functionfn2(){setTimeout(function(){fn2Timeout();fn3();},0);}但是这相当于把原来的fn2Timeout去掉,换成新的函数,并且然后插入原来的fn2Timeout和fn3。这种动态改变原有功能的方法有一个专门的术语叫做MonkeyPatch。按照我们程序员的口头禅:“做了就可以了”,但是写起来有点曲折,很容易把自己卷进去。有没有更好的办法?我们退一步讲,在fn2Timeout完全执行完之前不要强制执行fn3,而是在fn2Timeout函数体的最后一行调用:functionfn2(){setTimeout(functionfn2Timeout(){console.log('secondcall');fn3();//Note{1}},0);}这样看起来好多了,但是定义fn2的时候,没有fn3,这个fn3是从哪里来的呢?还有一个问题。由于fn3要在fn2中调用,所以我们不能通过stack.forEach来调用fn3,否则fn3会被调用两次。我们不能在fn2中硬编码fn3。相反,我们只需要在fn2Timeout的末尾找到栈中fn2的下一个函数,然后调用:;},0);}这个next函数负责在栈中找到下一个函数并执行它。现在让我们执行下一个:varindex=0;函数next(){varfn=stack[index];索引=索引+1;//其实也可以用shift把fn取出来,索引会加1,从而达到取出下一个函数的目的。Next是这样使用的:varstack=[];//defineindexandnextfunctionfn1(){console.log('thefirstcall');下一个();//堆栈中的每个函数都必须调用`next`};stack.push(fn1);functionfn2(){setTimeout(functionfn2Timeout(){console.log('secondcall');next();//调用`下一个`},0);}堆栈。push(fn2,function(){console.log('第三次调用');next();//最后一个调用不了,调用也没用});下一个();//Callnext,finally依次输出'firstcall','secondcall','thirdcall'。现在stack.forEach这一行已经被删除了,我们自己调用一次next,next会在栈中找到第一个函数fn1并执行,在fn1中调用next寻找并执行下一个函数fn2,在fn2中调用next,等等等等。接下来必须调用每个函数。如果不是写在某个函数里面,那么这个函数执行完程序就会直接结束,没有继续执行的机制。理解了这个函数队列的实现之后,你应该能够解决下面的面试题://实现一个LazyMan,调用方式如下:LazyMan("Hank")/*输出:Hi!这是汉克!*/LazyMan("Hank").sleep(10).eat("dinner")输出/*输出:嗨!ThisisHank!//Waitfor10seconds..Wakeupafter10Eatdinner~*/LazyMan("Hank").eat("dinner").eat("supper")/*输出:嗨,这是Hank!Eatdinner~Eatsupper~*/LazyMan(“Hank”).sleepFirst(5).eat(“supper”)/*等待5秒,输出Wakeupafter5HiThisisHank!吃晚饭*///等等。Node.js中著名的connect框架就是这样实现中间件队列的。有兴趣的可以看看它的源码或者这篇解读《何为 connect 中间件》。如果细心的话,你可能会看到这个next暂时只能放在函数的末尾。如果放在中间,还是会出现原来的问题:functionfn(){console.log(1);下一个();控制台日志(2);//如果next()调用异步函数,console.log(2)将首先执行。redux和koa可以使用不同的实现,将next放在函数中间,执行完后面的函数后返回执行next下面的代码就很巧妙了。有时间再写吧。
